❐ Description
Enum과 Sealed 모두 제한된 유형의 계층 구조를 표현하는데 사용되지만, 각각의 목적과 사용 방식에 차이가 있다.
오늘은 Sealed class/interface의 특징과 enum class와 차이를 공부해보자.
❐ Sealed Class
1. 정의
sealed class는 같은 파일 내에서만 하위 클래스를 가질 수 있도록 제한된 계층 구조를
제공하는 추상 클래스의 한 종류다.
sealed class Shape {
class Circle(val radius: Double) : Shape()
class Rectangle(val width : Double, val height : Double) : Shape()
object NotAShape : Shape()
}
2. 특징
- 제한된 상속(Restrictive Inheritance)
- sealed class의 모든 하위 클래스는 컴파일 시점에 확정된다.
- 따라서 상속 구조를 더 안전하고 제어된 형태로 유지할 수 있다.
- 완전한 `when` 표현식 (Exhaustive `when` Expressions)
- sealed class를 when 표현식에서 사용할 때, 코틀린은 모든 가능한 하위 클래스를 처리하도록 강제한다.
- 따라서 코드의 안정성이 높아지고 유지보수가 쉬워진다.
- 향상된 타입 안전성 (Improved Type Safety)
- 상속을 제한함으로써, 예기치 않은 구현이 추가되어 설계를 깨트리는 일을 방지할 수 있다.
- 추상 클래스로 직접 객체 인스턴스 생성이 불가하다.
3. Sealed Class 사용해보기
Sealed Class는 when 절과 함께 가장 많이 사용되곤 한다.
fun describeShape(shape: Shape) {
return when (shape) {
is Shape.Circle -> "~~~"
is Shape.Rectangle -> "~~~"
Shape.NotAShape -> "~~~"
}
}
만약 위에서 Shape에 정의된 클래스를 모두 처리하지 않으면 컴파일 에러가 발생하게 된다.
4. Sealed Class 상속 받기
sealed class Shape {
class Circle(val radius: Double) : Shape()
class Rectangle(val width : Double, val height : Double) : Shape()
class Triangle(val hypotenuse : Double, val height : Double) : Shape() {
// override equals & hashCode
}
object NotAShape : Shape()
}
기본적으로 같은 클래스 내에서 상속을 정의해야 한다. 또한 상태(변수)가 있거나 equals를 override할 경우에는
위와 같이 클래스로 상속 받을 수 있다. 마지막으로 상태가 없는 경우 불필요한 인스턴스의 생성을 방지하기 위해
object 키워드를 사용하여 싱글톤 객체로 정의할 수 있다.
5. Sealed Interface
sealed interface는 `Kotlin 1.5`부터 도입된 기능으로, 특정 계층의 인터페이스 구현을 제한하는 역할을 한다.
기존의 sealed class와 유사하게, sealed interface를 구현할 수 있는 클래스나 인터페이스를 같은 패키지
내에서만 허용한다.
Sealed Interface는 다음의 특징을 갖는다.
- 상속 제한
- Sealed interface를 구현하는 클래스나 인터페이스는 같은 패키지 내에서만 정의할 수 있다.
- 패키지를 벗어낫 곳에서는 sealed interface를 구현할수 없다.
- 추상 인터페이스 기능 유지
- sealed class와 달리 상태(필드)를 가질 수 없다.
- 따라서 인터페이스로서의 추상적인 계약(Contract)만 정의할 수 있다.
- when 표현식에서 활용 가능
- sealed interface를 기반으로 when 문을 사용할 때, 모든 구현체를 명시하면 else 필요없음.
- 이는 sealed 클래스와 동일한 이점이다.
sealed interface는 구현을 제한해야 하지만 상태(필드)를 포함할 필요가 없을 때나,
다중 구현이 필요한 경우에 사용할 수 있다.
6. Sealed Class의 계층 구조
sealed class Expression {
abstract fun evaluate(): Int
class Constant(val value: Int) : Expression() {
override fun evaluate() = value
}
class Sum(val left: Expression, val right: Expression) : Expression() {
override fun evaluate() = left.evaluate() + right.evaluate()
}
class Product(val left: Expression, val right: Expression) : Expression() {
override fun evaluate() = left.evaluate() * right.evaluate()
}
}
fun main() {
val expr = Sum(Constant(3), Product(Constant(2), Constant(5)))
println(expr.evaluate()) // (3 + (2 * 5)) = 13
}
❐ Enum Class와 차이
항목 | Sealed Class | Enum Class |
목적 | 제한된 계층 구조 표현 (확장 가능) | 고정된 값(상수)의 집합 표현 |
하위 타입 정의 | 여러 개의 하위 클래스를 가질 수 있음 | 정해진 상수 값만 가질 수 있음 |
when 표현식 | 컴파일러가 when 표현식에서 모든 하위 타입을 처리했는지 검사 | 강제적으로 when을 수정하도록 유도하지 않음. |
❐ 언제 Sealed를 쓰고, 언제 Enum을 사용해야 할까?
1. Sealed Class
- 특정 타입의 확장 가능성을 고려해야 할 때
- 각 타입마다 고유한 속성이나 동작이 필요할 때
- 상태 관리가 중요한 State 패턴이나 Reesult 패턴 구현시
2. Enum Class
- 고정된 상수들의 집합을 정의할 때
- 간단한 값 기반 분기 처리
- 모든 상수가 동일한 속성과 동작을 가질 때