이번 글은 Java 17의 대표 기능이면서도 처음 보면 가장 이해하기 어려운 문법이다.
많은 사람들이
"상속을 막는 건 final 아닌가?"
라고 생각한다.
맞다.
하지만 sealed class는 상속을 막는 것이 아니라 "상속 가능한 대상을 제한하는 것"이다.
이 개념을 이해하면 다음과 같은 코드가 왜 가능한지 이해하게 된다.
sealed interface Shape
permits Circle, Rectangle {
}
record Circle(int radius)
implements Shape {
}
record Rectangle(int width, int height)
implements Shape {
}
double area = switch (shape) {
case Circle c -> ...
case Rectangle r -> ...
};
여기에는 default가 없다.
왜 컴파일 오류가 발생하지 않을까?
이번 글에서 그 이유까지 설명한다.
sealed class(Java 17)
상속을 완전히 막는(final)이 아니라, 상속 가능한 클래스를 개발자가 직접 지정하는 기능
먼저 final부터 이해하자.
public final class Animal {
}
이제
class Dog extends Animal {
}
를 작성하면
컴파일 오류가 발생한다.
즉,
final
↓
아무도 상속 불가
이다.
그런데 현실에서는 이런 경우가 많다.
결제 시스템이 있다고 하자.
Payment
↓
CardPayment
CashPayment
PointPayment
이 세 종류만 존재해야 한다.
그런데 누군가
class BitcoinPayment
extends Payment {
}
를 만들었다.
설계자는
CardPayment
CashPayment
PointPayment
이 세 개만 허용하고 싶은데...
라는 상황이 된다.
sealed 등장
public sealed class Payment
permits CardPayment,
CashPayment,
PointPayment {
}
이제
class CardPayment
extends Payment {
}
가능하다.
하지만
class BitcoinPayment
extends Payment {
}
는
컴파일 오류이다.
즉,
sealed
↓
상속 가능
↓
하지만 허용한 클래스만 가능
이다.
permits란?
sealed class Payment
permits
CardPayment,
CashPayment,
PointPayment
permits는
"상속을 허용하는 목록(Whitelist)"
이라고 생각하면 된다.
그림으로 보면
Payment
│
┌──────────────┼──────────────┐
│ │ │
CardPayment CashPayment PointPayment
│ │ │
O O O
BitcoinPayment
│
X
sealed를 상속받으면 반드시 선택해야 한다.
다음 코드는 컴파일 오류이다.
class CardPayment
extends Payment {
}
왜?
sealed를 상속받은 클래스는
반드시
final
sealed
non-sealed
중 하나를 선택해야 한다.
1. final
final class CardPayment
extends Payment {
}
의미
Payment
↓
CardPayment
↓
더 이상 상속 금지
2. sealed
sealed class CardPayment
extends Payment
permits VisaCard,
MasterCard {
}
의미
Payment
↓
CardPayment
↓
VisaCard
MasterCard
여기서도 다시 상속을 제한한다.
3. non-sealed
non-sealed class CardPayment
extends Payment {
}
의미
Payment
↓
CardPayment
↓
누구나 자유롭게 상속 가능
sealed 제한을 해제하는 것이다.
왜 이런 기능이 필요할까?
가장 큰 이유는
컴파일러가
모든 하위 타입을 알 수 있기 때문이다.
기존 switch
if (shape instanceof Circle) {
}
else if (shape instanceof Rectangle) {
}
컴파일러는
혹시 Triangle도 있을 수 있지 않을까?
를 모른다.
그래서
default 필요
하다.
sealed + Pattern Matching
sealed interface Shape
permits Circle,
Rectangle {
}
return switch (shape) {
case Circle c -> ...
case Rectangle r -> ...
};
컴파일러는
Shape
↓
Circle
Rectangle
↓
끝
↓
모든 경우 처리 완료
를 알고 있다.
그래서
default
가 없어도 된다.
이것을 Exhaustiveness Checking이라고 한다.
컴파일러가 모든 경우(case)가 처리되었는지 검사하는 기능
예를 들어
sealed interface Shape
permits Circle,
Rectangle,
Triangle {
}
그런데
switch (shape) {
case Circle c -> ...
case Rectangle r -> ...
}
컴파일러는
Triangle 처리가 없습니다.
라고 알려준다.
런타임이 아니라
컴파일 단계에서 오류를 발견한다.
record와 최고의 궁합
record는
record Circle(int radius)
implements Shape {
}
처럼
기본적으로 immutable이고 final이다.
그래서
sealed interface Shape
permits Circle,
Rectangle {
}
와 매우 잘 어울린다.
실제 Java 최신 코드 스타일은
sealed interface
↓
record 구현체
↓
Pattern Matching switch
조합을 많이 사용한다.
실무 예제
결제
sealed interface Payment
permits CardPayment,
CashPayment,
PointPayment {
}
switch (payment) {
case CardPayment card -> ...
case CashPayment cash -> ...
case PointPayment point -> ...
}
이벤트
sealed interface Event
permits LoginEvent,
LogoutEvent,
OrderEvent {
}
switch (event) {
case LoginEvent login -> ...
case LogoutEvent logout -> ...
case OrderEvent order -> ...
}
API Result
sealed interface ApiResult
permits Success,
Error {
}
switch (result) {
case Success success -> ...
case Error error -> ...
}
Spring에서는 많이 사용할까?
Spring Framework 자체는
많이 사용하지 않는다.
하지만
최근 Java 17+, Spring Boot 3.x 프로젝트에서는
Domain Event
Command
Result
State
같이
경우의 수가 명확한 객체에서
점점 많이 사용되고 있다.
특히
record
+
sealed class
+
Pattern Matching switch
를 함께 사용하는 코드가 계속 증가하는 추세이다.
final과 sealed 차이
| 상속 완전 금지 | 지정한 클래스만 상속 가능 |
| 확장 불가 | 제한된 확장 가능 |
| 모든 하위 클래스 차단 | 설계자가 허용한 하위 클래스만 허용 |
sealed와 non-sealed 차이
sealed
↓
허용한 클래스만 상속 가능
non-sealed
↓
다시 자유롭게 상속 가능
면접 포인트
sealed class란?
상속을 완전히 금지하는 것이 아니라, 개발자가 지정한 클래스만 상속할 수 있도록 제한하는 Java 17 기능이다.
permits란?
상속을 허용하는 하위 클래스 목록을 명시하는 키워드이다.
sealed를 상속받은 클래스는 무엇을 선언해야 하는가?
반드시 다음 중 하나를 선언해야 한다.
- final
- sealed
- non-sealed
Pattern Matching for switch와 왜 잘 어울리는가?
컴파일러가 모든 하위 타입을 알고 있으므로,
switch (shape) {
case Circle c -> ...
case Rectangle r -> ...
}
처럼 default 없이도 모든 경우를 처리했는지 컴파일 시점에 검사(Exhaustiveness Checking)할 수 있기 때문이다.
다음 글
다음은 Optional 심화이다.
여기서는 단순한 Optional.ofNullable() 사용법이 아니라,
- Optional이 왜 만들어졌는지
- 왜 필드와 파라미터에는 Optional을 권장하지 않는지
- map(), flatMap(), filter()의 내부 흐름
- orElse()와 orElseGet()의 성능 차이
- 실무에서 Optional을 남용하면 안 되는 이유
까지 깊이 있게 설명하겠다.
'language > java' 카테고리의 다른 글
| Stream 개선사항(Java 9~21) (0) | 2026.06.17 |
|---|---|
| Optional 심화 (0) | 2026.06.17 |
| record(Java 16) (0) | 2026.06.17 |
| Pattern Matching for switch(Java 21) (0) | 2026.06.17 |
| Switch Expression(Java 14) (0) | 2026.06.17 |
댓글