
안녕하세요.
오늘은 Sealed Class에 대해서 알아보도록 하겠습니다.
상속
Sealed Class를 알아보기 전, 상속에 대해 짚고 넘어가겠습니다.
아래와 같이 상속을 받는 클래스가 있습니다.

컴파일 타임 때
- Dog는 Animal을 상속받는 것을 알 수 있습니다.
- 오버라이드가 안됐다고 에러를 보여준다던지 등을 컴파일 때 잡을 수 있습니다.
- Animal은 어떤 클래스가 자신을 상속받는지 모릅니다.
하지만 Sealed Class는 컴파일 타임 때 자신의 하위 클래스 타입이 무엇인지 알 수 있습니다.
-> 컴파일 타임에 모든 가능한 하위 타입을 처리하도록 보장할 수 있습니다.
사용 예제
먼저, Sealed Class를 사용하지 않은 예제를 한번 보겠습니다.
else를 사용하지 않으면 컴파일 에러가 납니다.
abstract class State {}
class Idle: State()
class Attacked: State()
class Running: State()
fun getState(state: State): String {
return when (state) {
is Idle -> "Person is doing nothing"
is Attacked -> "Person is walking"
is Running -> "Person is running"
else -> "else"
}
}
Sealed Class를 사용하면 else 분기를 없앨 수 있습니다.
sealed class State {}
object Idle : State()
object Attacked : State()
object Running : State()
fun getState(state: State): String {
return when (state) {
is Idle -> "Person is doing nothing"
is Attacked -> "Person is walking"
is Running -> "Person is running"
}
}
컴파일 타임 때 sealed Class를 상속받는 자식 클래스가 무엇인지 전부 알기 때문에 가능합니다.
else가 없어도 위의 cases 만으로 모든 경우를 다 커버한다고 컴파일이 아는 것이죠.
특징
Sealed Class를 사용하기 위해선 독특한 특징이 하나 있습니다.
Sealed Class와 이를 상속받는 모든 클래스들은 하나의 파일 안에 위치해야 합니다.
sealed class Shape
data class Circle(val radius: Double) : Shape()
data class Rectangle(val height: Double, val width: Double) : Shape()
object UnknownShape : Shape()
이를 통해 다른 위치에서 의도치 않게 Sealed Class를 상속하지 않게돼 코드의 안정성이 확보됩니다.
선언
상속받는 클래스들이 구현량이 많지 않다면,
상위 클래스의 내부 클래스로 밀어 넣을 수 있습니다.
sealed class Shape {
data class Circle(val radius: Double) : Shape()
data class Rectangle(val height: Double, val width: Double) : Shape()
object UnknownShape : Shape()
}
그룹화가 되면서 코드 가독성이 더 좋아지게 됩니다.
대신 사용할 때는 'Shape.' 을 붙여서 사용해야 합니다.
그렇지만, 저는 가독성이 더 좋아져서 오히려 좋은 것 같습니다.
Before
fun describeShape(shape: Shape): String = when (shape) {
is Circle -> "A circle with radius ${shape.radius}"
is Rectangle -> "A rectangle with height ${shape.height} and width ${shape.width}"
UnknownShape -> "Unknown shape"
}
After
fun describeShape(shape: Shape): String = when (shape) {
is Shape.Circle -> "A circle with radius ${shape.radius}"
is Shape.Rectangle -> "A rectangle with height ${shape.height} and width ${shape.width}"
Shape.UnknownShape -> "Unknown shape"
}
vs Enum
Sealed Class는 하나의 집합으로 표현이 가능한데요.
Enum Class 역시 집합을 표현할 수 있습니다.
그렇다면, 이 둘의 차이는 무엇일까요?
복잡성
enum
단순한 상수 집합을 표현하는데 사용됩니다.
각각의 상수는 동일한 타입입니다.
enum class Color {
RED, GREEN, BLUE
}
sealed class
더 복잡한 데이터 구조를 표현할 수 있습니다.
각각의 하위 클래스는 서로 다른 속성과 동작을 가질 수 있습니다.
sealed class Shape
data class Circle(val radius: Double) : Shape()
data class Rectangle(val height: Double, val width: Double) : Shape()
object UnknownShape : Shape()
다형성
enum
enum은 다형성을 지원하지 않습니다.
(다형성 : 동일한 인터페이스나 상위 클래스에서 파생된 여러 하위 클래스가 각기 다른 방식으로 동작할 수 있는 능력)
모든 상수는 같은 형태의 속성만을 가질 수 있습니다.
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
sealed class
하위 클래스마다 다른 속성이나 동작을 정의할 수 있기 때문에 다형성을 완벽하게 지원합니다.
즉, 데이터 타입의 다양성과 상태 변화를 나타내는데 더 적합합니다.
Sealed Interface
class와 interface의 차이처럼
sealed class와 sealed interface의 차이는 동일합니다.
- 인터페이스는 구현을 하지 않아도 된다
- 인터페이스는 다중 상속이 가능하다
sealed interface Movable {
fun move(): String
}
sealed interface Flyable {
fun fly(): String
}
sealed interface Swimmable {
fun swim(): String
}
// 클래스를 정의하면서 두 개 이상의 sealed interface를 구현할 수 있습니다.
data class Duck(val name: String) : Movable, Flyable, Swimmable {
override fun move() = "$name is walking."
override fun fly() = "$name is flying."
override fun swim() = "$name is swimming."
}
data class Airplane(val model: String) : Movable, Flyable {
override fun move() = "Airplane $model is moving on the runway."
override fun fly() = "Airplane $model is flying."
}
object Fish : Swimmable {
override fun swim() = "Fish is swimming."
}
사용 사례
Sealed Class는 어떤 데이터들의 집합으로 사용될 수 있습니다.
Error의 종류로 표현할 수 있습니다.
sealed class Error(val message: String) {
class NetworkError : Error("Network failure")
class DatabaseError : Error("Database cannot be reached")
class UnknownError : Error("An unknown error has occurred")
}
안드로이드에서는 UI State를 나타낼 때 Sealed Class가 많이 사용됩니다.
sealed class UIState {
data object Loading : UIState()
data class Success(val data: String) : UIState()
data class Error(val exception: Exception) : UIState()
}
fun updateUI(state: UIState) {
when (state) {
is UIState.Loading -> showLoadingIndicator()
is UIState.Success -> showData(state.data)
is UIState.Error -> showError(state.exception)
}
}
정리
Sealed Class는 아래와 같은 특성을 띄는 특수한 클래스 입니다.
- 계층 구조를 제한하고, 하위 클래스들이 명시적으로 정의되도록 강제하는 클래스
- 안전한 패턴 매칭을 위해 주로 사용되고, when 구문과 함께 사용될 때 모든 경우를 다뤘는지 컴파일러가 확인해줌
- 클래스의 상속 확장을 의도적으로 막고, 특정 파일 내에서만 하위 클래스를 정의할 수 있게 함으로써, 더 예측 가능하고 안정적인 코드 구조를 유지할 수 있음
참고 자료
[Kotlin] Kotlin sealed class란 무엇인가?
sealed class의 등장 배경 여러 자식 Class들이 하나의 부모 Class를 상속 받았다고 했을 때 컴파일러는 부모 Class를 상속 받은 자식 Class들이 있는지 알지 못한다. 예를 들어보자. 우리가 사용자의 런닝
kotlinworld.com
https://medium.com/@sandeepkella23/everything-about-sealed-classes-in-kotlin-de525b98b192
Everything about Sealed Classes in kotlin
Sealed classes in Kotlin are a powerful feature that allows you to define a restricted hierarchy of classes. They are particularly useful…
medium.com
https://kotlinlang.org/docs/sealed-classes.html#payment-method-handling
Sealed classes and interfaces | Kotlin
kotlinlang.org
'Kotlin' 카테고리의 다른 글
[Kotlin] 함수형 프로그래밍 (2) | 2024.10.29 |
---|---|
[Kotlin] Scope Function (2) | 2024.10.11 |
[Kotlin] Extension Function (확장 함수) (0) | 2024.09.10 |
[Kotlin] Null Safety (0) | 2024.07.29 |
[Kotlin] Coroutine (3) - 예외 처리 (1) | 2023.12.01 |