본 시리즈는 저서 『이것이 자바다(신용권, 임경균 저)』 를 길잡이 삼아 자바를 다시 배워가는 기록입니다. 지난 글 '이것이 자바다 3'을 통해서 책의 Part02: 객체지향 프로그래밍 영역에 진입했습니다.
객체지향 프로그래밍 영역에 들어갔지만 설계에 대한 책이 아니라 프로그래밍 언어 Java에 대한 책이므로 Java에서 객체지향을 위해 사용하는 도구들을 알게 될 것입니다. 이에 지난 글에서는 메모리 사용 영역과 주소값을 저장하는 레퍼런스 타입을 보았습니다.
레퍼런스 타입을 데이터 위치를 다루는 기술로 생각한다면, 이번에 다룰 클래스는 생성될 객체의 명세(spec)을 정의하는 것 입니다. 처음 클래스를 접하시는 분들은 명세(spec), 청사진(blueprint)로 비교되는 것이 이해가 어려울 수 있습니다. 제 나름의 설명을 해보자면 100개의 동일한 초콜릿이 있고, 한 아이에게 하나의 초콜릿을 주었습니다. 이 아이가 그 초콜릿을 잃어버려서 다른 동일한 초콜릿을 주었습니다. 하지만 그 초콜릿이 아니라고 합니다. 다른 동일한 초콜릿을 주었지만 처음 준 그 초콜릿은 아닙니다. 여기서 아이에게 제일 처음 준 100개의 초콜릿 중 하나는 실체(object)입니다. 99개의 다른 동일한 초콜릿들은 모두 같은 레시피로 만들어진 것이고요. 100개의 동일한 초콜릿을 만들 수 있게 한 레시피가 바로 Class 입니다.
Ch06. 클래스
객체지향 프로그래밍(OOP)
객체지향 프로그래밍은 시스템을 상호작용하는 독립적인 객체(Object)들의 집합으로 파악하는 패러다임이다. 여기서 객체는 상태(Field)와 행위(Method)를 하나로 묶은 식별 가능한 실체를 의미한다.
객체 간의 관계
- 집합 관계 (Has-A): A가 A1, A2, A3 등으로구성된다. (예: 게임 캐릭터가무기, 방어구 등으로 구성됨. )
- 사용 관계 (Uses-A) : B가 A를 사용한다. (예: 게임 캐릭터가 회복 물약을 사용한다.)
- 상속 관계 (Is-A): A는 C의 특징을 물려받는다. (예: 게임 캐릭터의 특정 직업이 기본 캐릭터의 특징을 물려받는다.)
객체지향 프로그래밍의 3대 특징 - 캡슐화, 상속, 다형성
- 캡슐화(Encapsulation): 데이터(필드)와 동작(메소드)를 하나로 묶고, 실제 구현 내용을 외부에 감추는 것. 자바는 이를 위해서 접근 제한자(Access Modifier)를 제공한다.
- 상속(Inheritance): 부모 객체(상위 객체)가 자식 객체(하위객체)에게 가지고 있는 필드와 메소드를 물려준다. 부모의 것만 수정해도 자식에게 모두 반영되기 때문에 코드 재사용성을 높여주고 유지보수 시간을 최소화 시킨다.
- 다형성(Polymorphism): 사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질이다. 부품을 교환해서 성능이 변경되는 것과 같다. 상속과 인터페이스 구현을 통해 처리되며, 자동 타입 변환과 재정의(override)를 이용한다.
클래스와 객체의 생성
클래스(Class)는 객체(object)를 생성하기 위한 메타데이터(Metadata)이자 설계도이다. 이 설계도(class)를 바탕으로 Heap 메모리에 실제 점유 공간을 확보한 실체를 인스턴스(instance)라고 한다.
개발자 --- 설계 ----> 클래스(설계도) ----- 인스턴스화 ---> 인스턴스(객체)
// 하나의 파일에 복수 개의 클래스 선언 가능
public class Warrior{
// ...
}
class Sword{
// ...
}
복수 개의 클래스 선언이 포함된 소스 파일을 컴파일하면 바이트코드 파일(.class)은 클래스 선언 수만큼 생긴다. 소스 파일명과 동일한클래스만 public class로 선언할 수 있으므로, 특별한 이유가 없다면 소스 파일 하나당 클래스 하나를 선언하는 것이 좋다.
클래스의 용도는 크게 두 가지 이다.
- 라이브러리: 다른 클래스에서 이용할 목적으로 만들어진 클래스
- 실행 클래스: main() 메소드를 가지고 있는 실행 클래스. 프로그램 실행 시 라이브러리 클래스를 내부에서 이용한다.
클래스의 구성 멤버: 필드, 생성자, 메소드
필드(Field): 객체의 데이터(고유, 현재 상태, 부품)가 저장되는 곳
public class GameCharacter{
String jobName; // 고유 데이터
int hp; // 상태 데이터
boolean isAlive;
Sword sword; // 부품 데이터
}
생성자(Constructor)
new 연산자는 객체를 생성한 후, 이어서 생성자를 호출해서 객체를 초기화(필드 초기화 및 메소드 호출 등)하는 역할을 한다.
클래스에 명시적인 생성자 선언이 없으면 컴파일러가 기본 생성자를 바이트코드에 자동으로 추가한다.
public class Player {
String server = "아시아"; // 동일한 값은 선언 시 초기화
String nickname;
String accountId;
// 생성자 오버로딩: 다양한 초기화를 위해 매개변수를 달리하는 생성자 여러 개 선언
public Player(String nickname, String accountId){
// 매개 변수명과 필드명이 동일할 때 필드임을 구분하기 위해 this 키워드 사용
this.nickname = nickname;
this.accountId = accountId;
}
public Player(){
}
}
메소드(Method): 객체의 동작. 호출 시 실행되는 코드 블록
// 가변 길이 매개변수: 매개값의 개수를 모를 때 사용. 내부적으로 배열로 처리됨
int calculateTotalDamage(int ...hits){ .... };
int total = calculateTotalDamage(10, 25, 15);
- 메소드 오버로딩: 메소드 이름은 동일하지만 매개변수 타입, 개수, 순서가 다른 메소드를 여러 개 선언하는 것
- return 문은 메소드 실행을 강제 종료하고 호출한 곳으로 돌아간다. 그 이후 실행문을 작성하면 Unreachable code 컴파일 에러가 발생한다.
생성자 선언과 호출
new 연산자는 객체를 생성한 후, 연이어 생성자를 호출해서 객체를 초기화하는 역할을 한다. 객체를 초기화한다는 것은 필드값을 초기화하거나 메소드 호출을 통해서 객체를 사용할 준비를 한다는 것이다.
Player player = new Player(...);
위와 같은 형태로 객체를 생성하면, Stack 영역에 참조변수 player가 저장된다. 이 Stack 영역에는 객체의 실제 값이 아니라 Heap 영역에 생성된 객체의 메모리 주소(Reference)가 기록된다. Heap 영역에는 new 연산자로 생성된 실제 Player 객체가 있다. 객체의 필드 데이터들이 이곳에 존재하고 GC의 관리 대상이 된다.
- 모든 클래스는 하나 이상의 생성자가 존재한다. 개발자가 생성자를 선언하지 않았다면 컴파일러는 기본 생성자를 바이트 코드 파일에 자동으로 추가시킨다. 그러므로 생성자를 선언하지 않아도 new 연산자 뒤에 기본 생성자를 호출할 수 있다. 명시적으로 선언한 생성자가 있으면 컴파일러는 기본 생성자를 추가하지 않는다.
- 클래스가 public class면 기본 생성자도 public이 붙지만, default class이면 기본 생성자도 public이 붙지 않는다.
필드 초기화
- 객체마다 동일한 값을 가짐: 필드 선언 시 초기 값을 대입하는 게 좋다.
- 객체마다 다른 값을 가짐: 생성자에서 필드를 초기화하는 게 좋다.
public class Player{
String server = "아시아"; // 모든 객체 공통
String nickname; // 객체별 상이
}
생성자 오버로딩
생성자 오버로딩은 매개변수를 달리하는 생성자를 여러 개 선언하는 것이다. 매개 값으로 객체 필드를 다양하게 초기화하려면 생성 자 오버로딩이 필요하다. 생성자 간 오버로딩이 많아질 경우 생성자 간 중복 코드가 늘어날 수 있다. 생성자 간 공통 코드를 한 가지 생성자에 집중해서 작성하고 나머지는 this(...)를 이용해 공통코드를 가진 생성자를 호출하는방법으로 개선할 수 있다.
public class Player{
String server = "아시아";
String nickname;
String job;
public Player(String nickname, String job){
this.nickname = nickname;
this.job;
}
public Player(String nickname){
this(nickname, "수련생"); // this를 이용함
}
}
메소드 관련 내용
가변 길이 매개변수
int sum(int ...values){
// 위와 같은 형태로 작성
// 매개값들은 자동으로 배열 항목으로 반환되어 메소드에서 사용한다.
}
// 호출시에는 아래와 같은 형태로 처리 되어야 한다.
int sum = sum(new int[] {1, 2, 3});
- return문: 메소드 실행을 강제 종료하고 호출한 곳으로 돌아간다는 뜻. return 문 이후 실행문을 작성하면 Unreachable code라는 컴파일 에러가 발생한다.
- 메소드 오버로딩: 메소드 이름은 동일하지만 매개변수 타입, 개수, 순서가 다른 메소드를 여러 개 선언하는 것. 대표적으로 System.out.println() 메소드가 존재한다.
인스턴스 멤버와 정적(static) 멤버
- 인스턴스 멤버: 힙 영역에 생성된 개별 객체에 소속되어 객체 생성 후에만 사용할 수 있는 필드와 메소드.
- 정적 멤버: 클래스 로더를 통해 클래스가 로딩될 때 메소드 영역에 고정적으로 위치하여 객체 생성 없이도 사용할 수 있는 필드와 메소드
인스턴스 멤버는 힙 영역에 생성된 개별 객체에 소속되고, 정적 멤버는 클래스에 소속되어 클래스 로더가 클래스를 읽어올 때 메소드 영역에 고정적으로 위치한다. 이때, 인스턴스 멤버건 정적 멤버건 '메소드'는 모두 메소드 영역에 단 하나만 존재하고 공유된다. 메소드는 실행할 코드 데이터(명령어 덩어리)이므로, 인스턴스 멤버라고 하더라도 객체를 만들 때마다 힙 영역에 중복 저장하면 메모리가 낭비되기 때문이다.
어떤 상황에서 static을 붙여야 할까?
- 정적 필드: 객체마다 따로 가지고 있을 필요 없이, 항상 일정한 값이나 공유 데이터를 저장할때 선언한다.
- 정적 메소드: 내부 인스턴스 필드를 전혀 사용하지 않는, 순수한 유틸리티성 기능을 만들 때 정적 메소드로 선언하는 것이 좋다.
public class GameConfig{
// 모든 유저에게 공통적으로 적용되는 경험치 배율은 static으로 선언
static double expMultiplier = 1.5;
// 인스턴스 필드를 사용하지 않는 유틸리티성 메소드는 정적 메소드로 선언
static int applyBonousExp(int baseExp, int bonus){
return baseExp + bonus;
}
}
정적 멤버는 클래스 소속이므로 사용할 때 객체를 사용하지 않고 GameConfig.expMultiplier 처럼 클래스 이름으로 직접 접근하는 것이 정석이다. 객체 변수를 통해 ㅈ넙근할 수도 있지만, 이 경우 IDE에서 경고를 표시하며 클래스 이름으로 접근할 것을 권장한다.
정적블록: 복잡한 초기화 담당
보통 정적 필드는 선언과 동시에 static doulbe pi = 3.141592; 처럼 초깃값을 주는 것이 일반적이다. 하지만 조건문이 필요하거나 계산이 복잡하여 한 줄로 초기화할 수 없는 경우가 있다. 정적 멤버는 객체 생성시 호출되는 생성자를 사용하지 않는다. 이때, 정적 블록을 사용한다.
static{
// 클래스가 메모리로 로딩될 때 자동으로 딱 한 번 실행되는 구간
}
만약 클래스 내부에 여러 개의 정적 블록이 선언되어 있다면 위에서부터 작성된 순서대로 차례차례 실행된다.
정적 메소드와 정적 블록은 "겍체가 없는 상태에서도 실행될 수 있다"는 특징을 가진다.
- 인스턴스 멤버 사용 불가: 정적 메소드 내부에서는 힙 영역에 존재할지도 모르는 인스턴스 필드나 인스턴스 메소드를 사용할 수 없습니다. 객체가 생성되지 않은 시점에는 힙 영역에 아무것도 없기 때문이다.
- this 키워드 사용 불가: 같은 이유로, 객체 자신을 가리키는 참조 변수인 this 키워드도 정적 멤버 내부에서는 사용할 수 없다.
final 필드와 상수
- final 필드: 한번 초기값이 대입되면 값을 변경할 수 없는 필드(값의 변경을 막고 read only로만 허용)
- final 필드에 초기값을 줄 수 있는 방법은 2가지: 1. 필드 선언 시 초기값 대입 2. 생성자에 초기값 대입
- 상수: 불변의 공용의 값. 객체마다 저장할 필요가 없으므로 static final로 선언한다.
- static final MAX_LEVEl = 99; 같은 형태로 모든 이름을 대문자로 작성하고 _을 이용해서 잇는다.
패키지와 접근 제한자
자바에서 패키지는 내 컴퓨터의 일반적인 '폴더(디렉토리)'와는 조금 다르다. 패키지는 클래스의 일부분으로, 클래스를 고유하게 식별하는 역할을 한다. 예를 들어, Car라는 이름의 동일한 클래스가 존재할 때 패키지를 사용해서 com.acompany.Car와 com.bcompany.Car로 같은 Car라는 이름의 class를 구분할 수 있다.
패키지는 클래스 식별 용도로 이름에 완전히 종속되어 있기 때문에, 컴파일된 바이트 코드 파일(*.class)만 다른 디렉토리로 이동시키면 프로그램이 정상 동작하지 않는다. 반드시 해당 패키지 구조와 함께 움직여야 한다.
// 1. 패키지 선언: 항상 소스 파일의 최상단에 위치!
package com.mycompany.ch06;
// 2. 다른 패키지의 클래스를 사용하기 위한 import
import com.acom.Tire;
import com.bcom.Tire;
패키지 선언
패키지 디렉토리는 우리가 일일이 폴더를 만들 필요 없다. 자바 소스 파일 최상단에 패키지를 선언해 두면, 컴파일러가 이를 해석해 디렉토리로 자동으로 생성해 준다.
- 선언 위치: 반드시 소스파일 최상단
- 이름 짓는 관례: 모든 글자는 소문자로 작성한다. 전 세계에서 중복되지 않도록 도메인 이름 역순으로시작하고, 마지막에 프로젝트 이름을 붙이는 것이 일반적이다. com.회사명.프로젝트명
import문: 외부 클래스 불러오기
같은 패키지 안에 있는 클래스들은 아무런 조건 없이 서로 자유롭게 가져다 쓸 수 있다. 하지만 다른 패키지에 속한 클래스를 사용하려면 import 문을 사용해 컴파일러에게 어떤 패키지의 클래스인지 명시해 주어야 한다.
이때, 클래스 이름 충돌 문제로 컴파일 에러가 발생할 수 있다.
만약 com.acompany 패키지의 Tire와 com.bcompany 패키지의 Tire를 모두 둘다 import한 상태에서, 코드에 Tire t = new Tire(); 이라고 적으면 컴파일러는 어떻게 할까? 컴파일러는 Tire가 a 회사 것인지 b 회사 것인지 판단할 수 없어 컴파일 에러를 발생시킨다. 이럴 때는 import 문 대신, 코드 상에서 com.acompany.Tire t = new com.acompany.Tire(); 처럼 패키지명을 포함한 전체 이름을직접 작성해야 에러를 해결할 수 있다.
접근 제한자
접근 제한자가 필요한 이유는 중요한 필드와 메소드가 외부로 노출되지 않도록 함으로써 객체의 무결성(결점이 없는 성질)을 유지하기 위해서이다.
| 접근 제한자 | 제한 대상 | 제한 범위 |
| public | 클래스, 필드, 생성자, 메소드 | 없음 |
| protected | 필드, 생성자, 메소드 | 같은 패키지이거나, 자식 객체만 사용가능 |
| (default) | 클래스, 필드, 생성자, 메소드 | 같은 패키지 |
| private | 필드, 생성자, 메소드 | 객체 내부 |
getter와 setter
객체의 필드를 외부에서 마음대로 변경할 수 있는 경우 객체의 무결성이 깨질 수 있다. 이러한 문제점때문에 OOP에선 직접적인 외부에서 필드 접근을 막고, 그 대신 메소드를 통해 필드에 접근하는 것을 선호한다. 그 이유는 메소드가 데이터를 검증해서 유효한 값만 저장할 수 있게 한다. 이 역할을 하는 것이 setter이다.
싱글톤 패턴
애플리케이션 전체에서 단 한 개의 객체만 생성해서 사용하고 싶다면 싱글톤 패턴을 사용한다. 생성자를 private으로 제한해서 외부에서 new 연산자로 생성자를 호출할 수 없게 하는 것이 핵심이다.
public class GameManager {
// 1. 자신의 타입인 정적 필드를 하나 선언하고 자신의 객체를 생성해 초기화한다.
private static GameManager instance = new GameManager();
// 2. 외부에서 객체를 생성하지 못하도록 생성자를 private으로 선언한다.
private GameManager() {}
// 3. 외부에서 유일한 객체를 얻을 수 있도록 정적 메소드를 제공한다.
public static GameManager getInstance() {
return instance;
}
}
Ch07. 상속
자바에서 상속은 부모 클래스의 멤버(필드, 메소드)를 자식 클래스가 물려받는 것을 말한다. 이 물려 받는 것(재사용)을 통해서 새로운 클래스를 를만들 때 중복 되는 코드를 줄일 수 있다. 상속을 위해 extends 키워드를 사용하며, 자바는 클래스 간 단일 상속만을 허용합니다. 부모는 오직 하나만 가질 수 있다.
// 자식 클래스가 부모 클래스를 선택한다
public class 자식클래스 extends 부모클래스{
// ...
}
부모 객체가 먼저 태어난다.
new 자식클래스(); 코드로 자식 객체를 생성하면, 메모리(Heap) 내부에서는 부모 객체가 먼저 생성된 후 자식 객체가 생성된다. 자식 클래스의 생성자 첫줄에 super(); 가 개발자가 생략해도 컴파일러 선에서 자동으로 추가되어 부모 생성자를 추가하기 때문이다. 단, 부모 클래스에 기본 생성자가 없고, 매개변수가 있는 생성자만 존재한다면 자식 생성자에서 반드시 super(매개값, ...);을 명시적으로 호출해야 컴파일 에러가 나지 않는다.
public class 자식클래스 extends 부모클래스{
public 자식클래스(...){
super(); // 부모 생성자: 없으면 컴파일 과정에서 자동 추가됨
...
}
}
메소드 재정의(overriding)
부모에게 물려받은 행동(메소드)이 자식 클래스에게 맞지 않을 때, 자식 클래스에서 이를 새롭게 정의하는 것을 메소드 오버라이딩이라고 한다. 오버라이딩되면 부모 메소드는 가려지고, 자식 메소드가 우선적으로 호출된다.
@Override를 통해 자바 컴파일러(javac)에게 "이 메소드는 부모 메소드를 재정의한 것이니 에러를 확인해줘"라고 요청하게 된다. 만약 자식 클래스에서 오타가 나는 등 문제가 생기면 자바 컴파일러는 오버라이딩 규칙 위반으로 컴파일 에러를 발생시킨다. IDE에서 빌드 전부터 코드에 빨간 줄을 그어 경고해주는 것은 백그라운드에서 자바 컴파일러 기능을 실시간으로 돌리고 있기 때문이다.
메소드 오버라이딩 주의사항
- 부모 메소드의 선언부(리턴타입, 메소드 이름, 매개변수)와 동일해야 한다.
- 접근 제한을 더 강하게 오버라이딩할 수 없다(public -> private으로 변경 불가)
- 새로운 예외를 throws할 수 없다.
public class XXXX extends EEEEE{
@Override // 컴파일 시 정확히 오버라이딩 되었는지 체크 해줌(생략 가능)
public .... ....(){
super.부모메서드(); // 이를 통해서 부모 메서드를 호출할 수 있다
}
}
상속의 제어: final 클래스, final 메소드, protected
- final 클래스: 클래스 선언 시 final을 붙이면 최종 클래스가 되어 더 이상 상속할 수 없다. String 클래스가 대표적인 예이다.
- final 메소드: 메소드에 final이 붙으면 최종 메소드가 되므로, 자식 클래스에서 절대 오버라이딩할 수 없다.
- protected 접근 제한자: 같은 패키지끼리는 무조건 접근을 허용하고, 다른 패키지일 경우 오직 자식 클래스에게만 접근을 허용한다.
다형성: 타입 변환
다형성이란 "사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질"을 의미한다. 이를 구현하기 위해 자동 타입 변환(Promotion)과 메소드 오버라이딩이 사용된다.
자동 타입 변환(Promotion): 부모 타입 자식 변수를 자식 객체에 대입하는 것
Cat cat = new Cat();
Animal animal = cat; // 부모 타입으로 자동 타입 변환
이때 cat == animal을 비교하면 true가 나온다. 타입만 변경되었을 뿐, 힙 영역에 있는 실제 객체의 주소값은 완벽히 동일하기 때문이다.
- 변환 후의 제약: 부모 타입으로 변환되면 부모 클래스에 선언된 필드와 메소드만 접근 가능하다.
- 단, 하나의 예외: 만약 자식 클래스에 오버라이딩된 메소드가 있다면, 부모 변수로 호출하더라도 자식의 오버라이딩된 메소드가 호출된다. 자바의 이 특성이 다형성을 낳게 된다.
강제 타입 변환(Casting): 부모 타입으로 변환된 객체를 다시 자식 타입으로 되돌릴 때 사용
Parent parent = new Child(); // 자동 타입 변환
Child child = (Child) parent; // 강제 타입 변환 (자식의 고유 기능을 다시 써야 할 때)
필드와 매개변수 다형성
- 필드 다형성: 필드의 타입은 부모 타입으로 고정하고, 어떤 자식 객체(부품)을 장착하느냐에 따라 결과가 달라지는 것
- 매개변수 다형성: 메소드 선언 시 매개변수 타입을 부모 타입으로 설정하면, 메소드를 호출할 때 모든 자식 객체를인자로 넘겨줄 수 있어 확장성이 극대화된다.
instanceof 연산자: 객체 타입 확인
강제 타입 변환을 할 때는 원래 어떤 객체였는지 확인해야 한다. 잘못된 타입으로 캐스팅하면 ClassCastException이 발생한다. 원래 어떤 객체였는지 확인할 때 instance of 연산자를 사용한다.
// Java 12 이전 스타일
if (parent instanceof Child) {
Child child = (Child) parent;
child.childMethod();
}
// Java 12 이후 최신 스타일 (우측에 변수 바로 선언 가능)
if (parent instanceof Child child) {
child.childMethod(); // 캐스팅 코드 필요 없이 바로 사용!
}
추상 클래스
추상 클래스는 한 마디로 실체가 없는 개념을 코드로 박제하고, 이를 상속한 자식 클래스들이 나아가야 할 방향을 제시하는 가이드라인이다.
실체 클래스(객체를 직접 생성할 수 있는 클래스)들의 공통적인 특성(필드, 메소드)을 뽑아내어 선언하며, 클래스 계층 구조의 최상단이나 중간 단계에서 "이 클래스 계층은 무엇을 해야한다"라고 규칙을 정해준다. 이를 상속(extends)한 자식 클래스는 "그 기능을 어떻게 구현할 것인가"를 담당하게 된다.
추상 클래스는 실체가 없는 '추상' 클래스이므로 new 연산자를 통해서 객체를 생성하는 것이 불가능하다. 그렇지만 추상 클래스에도 생성자는 존재한다. 자식 객체가 생성될 때는 부모 객체가 먼저 생성되어야 하므로 super()를 호출한다. 추상 클래스 생성자는 자식 객체가 힙 영역에 생성될 때 부모 필드 초기화를 위해 반드시 필요하다.
추상 메서드: 자식 클래스에게 강제
추상클레스 안에는 일반 메소드도 넣을 수 있지만, 가장 핵심이 되는 것은 추상 메소드이다. 메소드선언부(리턴 타입, 이름, 매개변수) 만 똑같이 만들어 두고 내부 실행 내용은 비워둔 메소드이다. 자식 클래스마다 실행 내용이 달라질 때 이를 사용한다.
봉인된 클래스
Sealed class는 무분별한 하위 클래스의 생성을 방지하기 위해, 상속 가능한 자식 클래스를 설계자(개발자)가 명시적으로 제한하는 기능이다. Java 15부터 도입되었고, 누구나 상속할 수 있거나(default)와 누구도 상속할 수 없는(final)이란 극단적인 선택지 사이에서 "허가된 대상만 상속할 수 있는"이란 중간 지대의 제어권을 제공한다.
public sealed class Parent permits Child1, Child2 { ... }
위와 같이 permits 이후에 적힌 클래스만 상속을 할 수 있다. 상속받은 자식 클래스는 반드시 final, sealed, non-sealed 라는 세 가지 상태 중 하나를 선택해서 선언해야 한다.
- final: 더 이상 상속할 수 없는 최종 클래스로 선언한다.
- sealed: 자기 자신도 봉인된 클래스가 되어, 또 다른 특정 하위 클래스에게만 상속을 허용한다.
- non-sealed: 부모의 봉인을 해제한다. 이 시점부터는 일반 클래스처럼 누구나 상속받을 수 있는 상태가 된다.
후기
최근에는 학습을 다소 미뤄두고 있었습니다. 단순히 프로그래밍 언어의 문법만을 반복해서 적으며 공부하는 방식에 당연히 흥미를느끼지 못했고, 그로 인해 자연스레 손이 가지 않았기 때문입니다.
제 개인 철학이나 깊이 있는 생각을 담아내기에는 부족한 실력입니다. 하지만 블로그에 '이것이 자바다' 책을 기반으로 제 나름의 시리즈인 I Don't Know Java를 작성하는 것은 이미 시작한 만큼 계속할예정입니다.
참고자료
'Java' 카테고리의 다른 글
| [I Don't Know Java] 이것이 자바다 5 (0) | 2026.06.04 |
|---|---|
| [I Don't Know Java] 이것이 자바다 3 (0) | 2026.04.07 |
| [I Don't Know Java] 이것이 자바다 2 (0) | 2026.03.19 |
| [I Don't Know Java] 이것이 자바다 1 (0) | 2026.03.09 |