
sealed interface와 sealed class 차이는 상태 모델링을 할 때 자주 헷갈립니다. 둘 다 가능한 하위 타입을 제한하고 when에서 빠진 상태를 잡는 데 도움을 주지만, 설계 의도는 조금 다릅니다.
이 글에서는 sealed 계열을 문법 비교로 끝내지 않고, 상태 계층, 공통 데이터, 다중 구현 가능성, API 설계 관점에서 설명하겠습니다. sealed 상태 표현은 sealed class vs enum 글과도 이어집니다.

sealed interface와 sealed class 차이, 먼저 공통점부터 잡기
Kotlin 공식 문서의 sealed classes and interfaces 설명처럼 sealed class와 sealed interface는 직접 하위 타입을 제한합니다. 그래서 when에서 모든 경우를 다뤘는지 컴파일러가 도와줄 수 있습니다.
sealed interface UiState {
data object Loading : UiState
data class Success(val items: List<String>) : UiState
data class Error(val message: String) : UiState
}
fun render(state: UiState): String = when (state) {
UiState.Loading -> "로딩 중"
is UiState.Success -> "${state.items.size}개"
is UiState.Error -> state.message
}여기까지는 sealed class로도 비슷하게 표현할 수 있습니다. 차이는 이 상태들이 어떤 공통 구조를 가져야 하는지, 다른 타입 계층과 함께 움직여야 하는지에서 드러납니다.
sealed class가 자연스러운 경우
sealed class는 하위 타입들이 하나의 강한 부모 계층 아래에 있고, 공통 상태나 생성자, protected 멤버를 공유해야 할 때 자연스럽습니다.
sealed class PaymentResult(
val requestedAmount: Int,
) {
class Approved(amount: Int, val transactionId: String) : PaymentResult(amount)
class Rejected(amount: Int, val reason: String) : PaymentResult(amount)
}이처럼 모든 결과가 같은 부모의 공통 데이터를 공유한다면 sealed class가 읽기 쉽습니다. 상속 구조 자체가 모델의 중심일 때는 class 쪽이 더 분명합니다.
sealed interface가 자연스러운 경우
sealed interface는 어떤 타입들이 같은 역할을 구현한다는 점이 중요할 때 좋습니다. 특히 이미 다른 클래스를 상속해야 하거나, 여러 sealed 역할을 동시에 구현해야 할 가능성이 있다면 interface가 더 유연합니다.
sealed interface ScreenState
sealed interface Refreshable
data object Loading : ScreenState
data class Content(
val items: List<String>,
) : ScreenState, Refreshable
data class Failed(
val message: String,
) : ScreenState, Refreshable여기서 Content와 Failed는 화면 상태이면서 동시에 새로고침 가능한 상태라는 역할을 가질 수 있습니다. sealed class만으로는 이런 다중 역할 표현이 더 제한적입니다.
상태 모델링 기준으로 보는 선택
- 공통 데이터와 공통 구현이 중요하면 sealed class
- 역할의 닫힌 집합을 표현하고 싶으면 sealed interface
- 여러 타입 계층을 동시에 표현해야 하면 sealed interface
- 상태들이 강한 부모-자식 구조로 묶이면 sealed class
Compose UI 상태처럼 Loading, Success, Error가 느슨한 상태 집합이라면 sealed interface가 깔끔할 때가 많습니다. 반대로 결제 결과처럼 공통 생성자나 공통 속성이 모델의 중심이라면 sealed class가 자연스럽습니다.
data object와 함께 쓰면 더 읽기 좋아진다
값이 없는 단일 상태는 object 또는 data object로 표현하면 좋습니다. Kotlin의 data object는 sealed 계층 안에서 문자열 표현과 동등성 감각을 더 깔끔하게 만들어 줍니다. 이 부분은 코틀린 data object 글과 함께 보면 좋습니다.
실무에서 피해야 할 오해
- sealed interface가 항상 sealed class보다 최신이고 좋은 것은 아니다
- sealed class를 쓰면 무조건 OOP스럽고, interface를 쓰면 무조건 유연한 것도 아니다
- when exhaustive만 보고 선택하면 공통 상태와 타입 역할을 놓친다
- public API로 내보낼 때는 앞으로 확장 가능성을 더 신중히 봐야 한다
sealed 선택의 핵심은 하위 타입을 닫는다는 사실보다, 무엇을 닫고 싶은지입니다. 상속 계층을 닫을지, 역할 집합을 닫을지 먼저 정해야 합니다.
마무리
sealed class와 sealed interface는 둘 다 상태 모델링을 더 안전하게 만드는 도구입니다. 공통 구현과 부모 계층이 중요하면 sealed class, 역할 기반의 닫힌 타입 집합이 중요하면 sealed interface가 더 자연스럽습니다.