해당 글은 Java 17부터 생겨난
sealed
에 대한 Oracle의 공식 문서를 참고하여 작성했습니다.
Sealed
키워드는 다른 클래스 또는 인터페이스가 해당 클래스 또는 인터페이스를 상속하거나 구현하는 행위를 제한한다. permits
키워드와 함께 사용하여 해당 클래스 또는 인터페이스를 상속하거나 구현할 수 있는 클래스 또는 인터페이스를 명시한다.
객체지향 프로그래밍의 핵심 개념인 상속, 그리고 상속의 이점인 코드의 재사용에 반하는 개념이기도 하다. 우리가 알고 있는 Java의 객체지향 프로그래밍대로라면, 새로운 클래스를 만들 때 이미 존재하는 클래스 또는 인터페이스를 최대한 활용(상속 또는 구현을 통해)하여 코드 유지보수성과 재사용을 향상하고자 한다. 특히 하나의 클래스에 대한 수정 사항이 이 클래스를 상속하는 타 클래스에도 반영된다는 점이 유지보수 면에서 탁월하다.
그러면 Sealed
는 왜 존재하는가? 무슨 용도로 사용하는가?
객체지향 프로그래밍의 장점인 상속을 왜 제한하고 싶을까? 제한해서 얻는 이점은 무엇일까?
답은 개발자의 컨트롤에 있다. 상속을 허용한다는 것은 무궁무진한 가능성을 연다는 것과도 같다. 해당 클래스의 개발자는 다른 개발자들이 상속을 통해 클래스의 메서드를 오버라이딩하는 등, 클래스의 변형과 확장을 허용한다.
하지만 개발자가 이를 원하지 않는 경우도 분명 존재한다. 예를 들자면, Math
와 같은 라이브러리는 확장 가능성을 열어두는 게 좋을까?
특히 Sealed
키워드는 상속 및 구현을 무조건적으로 막는 것이 아니라, permits
을 통해 계층 구조를 허용하기도 하고, 개발자의 입맛에 따라 상속을 제한할 수 있다는 점이 사용하기 편리하다.
그러니까 개발자가 컨트롤할 수 있는 범위 내에서만 확장해야 할 때 또는 하고 싶을 때 활용성이 높다고 볼 수 있다.
클래스에 sealed
키워드를 붙이기 위해서는 다음 순서를 따른다
<접근제어자> - sealed class
- <클래스명> - extends
및 implements
- permits
- <허용하는 클래스 목록>
permits
키워드로 허용하는 클래스 목록은sealed
키워드에 해당하는 클래스를 상속할 수 있다.sealed
클래스를 상속하는 클래스에 대한 조건은 다음 섹션에서 설명한다.
sealed
클래스인 Shape
을 상속하는 3개 클래스 Circle
, Square
, Rectangle
클래스다
public sealed class Shape
permits Circle, Square, Rectangle {
}
final
클래스 Circle
은 더 이상 상속될 수 없다.
public final class Circle extends Shape {
public float radius;
}
non-sealed
클래스 Square
은 클래스의 상속을 제한하지도 방지하지도 않는다. Shaped
클래스가 모르는 클래스도 Square
클래스를 상속하여 간접적으로 상속을 허용할 수 있다.
public non-sealed class Square extends Shape {
public double side;
}
sealed
클래스 Rectangle
은 Shape
클래스와 마찬가지로 permits
키워드를 통해 자신을 상속할 수 있는 클래스를 명시한다.
public sealed class Rectangle extends Shape permits FilledRectangle {
public double length, width;
}
sealed
클래스가 상속을 허용하는 서브클래스를 같은 파일 내에 정의하면permits
키워드를 사용할 필요가 없다.
package com.example.geometry;
public sealed class Figure
// The permits clause has been omitted
// as its permitted classes have been
// defined in the same file.
{ }
final class Circle extends Figure {
float radius;
}
non-sealed class Square extends Figure {
float side;
}
sealed class Rectangle extends Figure {
float length, width;
}
final class FilledRectangle extends Rectangle {
int red, green, blue;
}
허용된 서브클래스 (permitted subclass
, 키워드는 아니지만, permits
키워드에 기반해서 허용해서 쓰이는 표현이다)는 다음과 같은 조건을 따른다
sealed
클래스가 접근할 수 있어야 한다. sealed
클래스를 직접적으로 상속해야 한다. 즉, sealed
클래스를 상속하는 클래스를 상속하는 것은 해당되지 않는다sealing
을 어떻게 진행할지 명시해야 한다.final
: 더 이상 상속을 허용하지 않는다sealed
: permits
으로 명시한 서브클래스에 대해서만 상속을 허용한다non-sealed
: 어느 서브클래스던 상속을 허용한다. 부모인sealed
클래스는 해당 키워드로 인한 상속을 방지할 수 없다.sealed
클래스와 같은 모듈 내에 존재해야 한다. sealed
클래스가 이름을 명시하지 않은 모듈 내에 존재하면, 같은 패키지 내에 존재해야 한다. Sealed
인터페이스도 sealed
클래스와 마찬가지로 sealed
제어자로 선언하면 된다. 다음 순서를 따른다.
<접근제어자> - sealed
- <인터페이스명> - extends
<확장하는 인터페이스 목록> - permits
<구현을 허용하는 클래스 목록 및 확장을 허용하는 인터페이스 목록>
다음은 Expr
라는 sealed
인터페이스와 이 인터페이스를 구현할 수 있는 클래스들이다.
package com.example.expressions;
public class TestExpressions {
public static void main(String[] args) {
// (6 + 7) * -8
System.out.println(
new TimesExpr(
new PlusExpr(new ConstantExpr(6), new ConstantExpr(7)),
new NegExpr(new ConstantExpr(8))
).eval());
}
}
sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {
public int eval();
}
final class ConstantExpr implements Expr {
int i;
ConstantExpr(int i) { this.i = i; }
public int eval() { return i; }
}
final class PlusExpr implements Expr {
Expr a, b;
PlusExpr(Expr a, Expr b) { this.a = a; this.b = b; }
public int eval() { return a.eval() + b.eval(); }
}
final class TimesExpr implements Expr {
Expr a, b;
TimesExpr(Expr a, Expr b) { this.a = a; this.b = b; }
public int eval() { return a.eval() * b.eval(); }
}
final class NegExpr implements Expr {
Expr e;
NegExpr(Expr e) { this.e = e; }
public int eval() { return -e.eval(); }
}
sealed
클래스 및 인터페이스의 permit
표현에 record
클래스도 포함될 수 있을까?
record
클래스는 기본적으로 final
임을 내포하고 있다. 그래서 허용된 서브클래스의 조건 중 하나인 final
, non-sealed
, sealed
키워드 중 final
을 충족한다 판단되어 키워드 없이도 sealed
클래스를 상속할 수 있다.
java.lang.Class
는 sealed
클래스 및 인터페이스와 관련한 메서드 2개를 가지고 있다.
java.lang.constant.ClassDesc[] permittedSubclasses()
: 해당 클래스가 sealed
이라면 해당 클래스의 모든 허용된 서브클래스를 배열에 담아 리턴한다. 해당 클래스가 sealed
가 아니라면 빈 배열을 리턴한다.boolean isSealed()
: 해당 클래스 또는 인터페이스가 sealed
라면 true
를 리턴한다. 아니라면 false
를 리턴한다.출처: 오라클