본문 바로가기
language/java

sealed class(Java 17)

by 죄니안죄니 2026. 6. 17.
반응형

이번 글은 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 차이

finalsealed
상속 완전 금지 지정한 클래스만 상속 가능
확장 불가 제한된 확장 가능
모든 하위 클래스 차단 설계자가 허용한 하위 클래스만 허용

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

댓글