하위 타입의 종류가 컴파일 시점에 고정된 클래스 계층.
sealed interface UiState {
data object Loading : UiState
data class Success(val items: List<Item>) : UiState
data class Error(val message: String) : UiState
}
무엇을 얻나 — 망라 검사
when (state) {
is UiState.Loading -> showSpinner()
is UiState.Success -> showItems(state.items) // 스마트 캐스트로 items 에 바로 접근
is UiState.Error -> showError(state.message)
// else 가 필요 없다 — 컴파일러가 전부임을 안다
}
새 하위 타입을 추가하면 모든 when이 컴파일 에러로 알려 준다. 처리를 빠뜨릴 수 없다.
else 를 쓰면 이 이점이 사라진다
- 새 타입이 추가돼도 조용히 else 로 흘러간다
- sealed 를 쓰는 의미가 없어진다
enum과 무엇이 다른가
enum class Status { LOADING, SUCCESS, ERROR } // 각 값이 '인스턴스 하나'
sealed interface UiState { ... } // 각 하위 타입이 '자기 데이터' 를 갖는다
-
enum — 상태의 종류만 필요하다. 값마다 데이터가 다르지 않다
-
sealed — 상태마다 다른 데이터를 들고 다녀야 한다
-
"성공이면 목록, 실패면 메시지" 처럼 데이터가 갈리면 sealed 다
왜 강력한가 — 불가능한 상태를 없앤다
// ❌ 플래그로 표현하면 말이 안 되는 조합이 만들어진다
data class State(
val isLoading: Boolean,
val items: List<Item>?,
val error: String?
)
// isLoading=true 인데 error 도 있는 상태가 표현 가능하다 — 실제로는 불가능한데
// ✅ sealed 로는 애초에 만들 수 없다
"불가능한 상태를 표현 불가능하게 만든다" 가 이 설계의 핵심 가치다.
제약과 최신 문법
하위 타입은 같은 패키지·같은 모듈 안에 있어야 한다 (Kotlin 1.5+)
- 라이브러리 사용자가 임의로 확장할 수 없다
sealed interface 가 sealed class 보다 유연하다
- 다중 상속이 가능하고, 상태를 여러 계층으로 분류할 수 있다
data object (Kotlin 1.9+)
- object 에도 toString·equals 가 자동 생성된다
면접 함정
- ❌ "sealed는 상속을 막는다" → 같은 모듈 안에서는 상속할 수 있다. 밖에서 못 할 뿐이다.
- ❌ "when에 else를 넣으면 안전하다" → 오히려 망라 검사를 잃는다.
Result 타입을 직접 만든다
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Failure(val error: Throwable) : Result<Nothing>
}
fun <T> Result<T>.getOrNull(): T? = when (this) {
is Result.Success -> data
is Result.Failure -> null
}
out T 와 Nothing 의 조합이 핵심이다
- Nothing 은 모든 타입의 하위 타입이라
- Failure 하나로 Result<무엇이든> 자리에 들어갈 수 있다
상태를 계층으로 나누기
sealed interface UiState
sealed interface Loadable : UiState // 여러 계층으로 분류할 수 있다 (interface 라서)
data object Loading : Loadable
data class Success(val items: List<Item>) : Loadable
data class Error(val msg: String) : UiState
코틀린 표준 Result와의 차이
kotlin.Result 는 value class 라 제약이 있다
- 반환 타입으로만 쓸 수 있었다 (지금은 완화됐지만)
- 실패 타입을 Throwable 로 고정한다
도메인 오류를 타입으로 표현하고 싶으면 직접 sealed 로 만드는 편이 낫다
- sealed interface LoginError { data object WrongPassword; data object Locked ... }