개요
객체지향 프로그래밍(OOP)의 핵심 중 하나인 상속은 강력한 유연성을 제공하지만, 때로는 과도한 자유가 설계의 무결성을 해치는 원인이 된다. 이전의 자바에서는 클래스의 확장을 막기 위해 'final' 키워드를 사용하거나 패키지 수준에서 접근을 제어해야 했으나, 이는 '전부 아니면 전무' 식의 극단적인 선택만을 강요했다. 하지만 Java 17에서 정식 도입된 'Sealed Classes'는 상속할 수 있는 하위 클래스를 명시적으로 제한함으로써, 유연성을 유지하면서도 도메인 모델의 계층 구조를 완벽하게 통제할 수 있는 새로운 메커니즘을 제공한다.
Sealed Classes의 핵심 개념과 클래스 선언 원형
Sealed Classes는 상위 클래스가 '자신을 상속받을 수 있는 하위 클래스'가 누구인지 직접 결정하는 방식이다. 이를 통해 개발자는 클래스 계층 구조를 닫힌 상태로 유지할 수 있으며, 의도치 않은 제3의 클래스가 상속을 통해 로직을 오염시키는 것을 방지할 수 있다.
클래스 선언의 기본 원형은 다음과 같다.
// sealed 키워드를 사용하고 permits 뒤에 상속을 허용할 클래스들을 나열한다.
public sealed class DomainEvent
permits UserCreated, UserDeleted, UserUpdated {
private final long timestamp = System.currentTimeMillis();
public long getTimestamp() {
return timestamp;
}
}
// permits에 명시된 클래스들은 반드시 부모 클래스를 상속받아야 한다.
public final class UserCreated extends DomainEvent { /* ... */ }
이 구조는 특히 라이브러리나 API 설계 시 유용하다. 설계자가 정의한 범위 내에서만 확장이 일어나도록 강제함으로써, 예상치 못한 하위 클래스의 등장으로 인한 런타임 오류를 컴파일 타임에 차단한다.
도메인 모델 설계의 무결성 확보
실무에서 Sealed Classes를 적용하면 데이터 흐름의 명확성을 확보할 수 있다. 예를 들어, 결과 상태를 표현하는 클래스를 설계하고 이를 실제 로직에서 활용하는 방식은 다음과 같다.
// 1. Sealed Interface 선언
public sealed interface Result<T> permits Success, Failure { }
public final record Success<T>(T value) implements Result<T> { }
public final record Failure<T>(Throwable error) implements Result<T> { }
// 2. 실제 활용 예시: 전수 검사(Exhaustiveness)를 통한 안전한 처리
public String handleResult(Result<String> result) {
return switch (result) {
case Success<String> s -> "성공: " + s.value();
case Failure<String> f -> "실패: " + f.error().getMessage();
// 모든 하위 클래스를 처리했으므로 default 문이 필요 없다.
};
}
이렇게 설계된 계층 구조는 다음과 같은 강력한 이점을 제공한다.
첫째, 타입 시스템의 엄격화이다. 'Result'의 하위 타입은 오직 'Success'와 'Failure'뿐임을 보장받는다.
둘째, Exhaustiveness(전수 검사)의 활용이다. 위 예시처럼 Switch Expressions와 결합할 때, 모든 케이스를 처리했는지 컴파일러가 검증해준다. 만약 새로운 하위 클래스가 추가되었음에도 처리 로직이 누락되었다면 컴파일러가 에러를 발생시켜 결함을 사전에 방지한다.
하위 클래스의 세 가지 제어 옵션
Sealed Class를 상속받은 하위 클래스는 반드시 다음 세 가지 키워드 중 하나를 사용하여 자신의 상속 가능 여부를 명시해야 한다. 부모 클래스의 봉인은 유지되지만, 자식 클래스 수준에서 후속 상속 정책을 결정하는 것이다.
public sealed class Parent permits ChildFinal, ChildSealed, ChildNonSealed { }
1.final: 더 이상의 상속을 허용하지 않는다. 계층의 마침표를 찍을 때 사용한다.
public final class ChildFinal extends Parent { }
2.sealed: 자신 또한 하위 클래스를 제한적으로 허용하는 봉인된 클래스임을 선언한다.
public sealed class ChildSealed extends Parent permits GrandChild { }
public final class GrandChild extends ChildSealed { }
3.non-sealed: 부모의 봉인은 그대로 유지되지만, 선언한 클래스 자신은 일반 클래스처럼 누구나 상속받을 수 있도록 개방한다.
public non-sealed class ChildNonSealed extends Parent { }
// ChildNonSealed는 일반 클래스가 되었으므로 permits 명단에 없어도 누구나 상속 가능하다.
public class AnyoneCanInherit extends ChildNonSealed { }
결론
Java 17의 Sealed Classes는 단순한 문법적 추가를 넘어 자바의 타입 시스템을 한 단계 더 진화시킨 도구이다. 이는 개발자가 설계한 도메인의 경계를 코드로써 명확히 규정할 수 있게 해준다.
상속은 더 이상 통제 불가능한 확장이 되어서는 안 된다. Sealed Classes를 통해 '의도된 확장'만을 허용함으로써 코드의 가독성과 유지보수성을 극대화해 보자. 런타임의 불확실성이 줄어들고, 컴파일 타임의 안정성이 강화된 견고한 Java 애플리케이션을 경험하게 될 것이다.
'Dev > Java' 카테고리의 다른 글
| Java Modern Syntax: instanceof 패턴 매칭 (0) | 2026.02.07 |
|---|---|
| Java Modern Syntax: Text Blocks (0) | 2026.02.06 |
| Java Modern Syntax : Optional (0) | 2026.01.10 |
| Java Modern Syntax : Lambda in switch (0) | 2026.01.08 |
| Java Modern Syntax : Record (0) | 2026.01.07 |