1. 개요
상태를 표현할 때 제일 먼저 손이 가는 건 enum 이다. LOADING, SUCCESS, ERROR 세 개 정의하고 when 으로 분기한다. 그런데 SUCCESS 일 때만 필요한 데이터, ERROR 일 때만 필요한 에러 메시지를 어디 둘지가 애매해진다. sealed class 는 이 문제, 그리고 “새 상태를 추가했는데 분기 처리를 빼먹는” 문제를 같이 해결한다.
2. 핵심 내용
2-1. enum 으로 안 되는 이유
enum class ApiState { LOADING, SUCCESS, ERROR }
var state = ApiState.SUCCESS
var data: User? = null // SUCCESS 일 때만 의미 있음
var errorMessage: String? = null // ERROR 일 때만 의미 있음enum 상수는 이름 하나가 전부다. 상태별로 다른 데이터를 들려면 결국 nullable 필드를 상태 개수만큼 따로 두고 “지금 상태에 맞는 필드만 본다” 는 암묵적 규칙을 사람이 지켜야 한다. state 는 SUCCESS 인데 data 가 null 인 조합도 타입 시스템이 막지 않는다. 이런 “말이 안 되는 조합이 컴파일된다” 는 게 enum 기반 상태의 근본 문제다.
2-2. sealed class 는 상태마다 다른 데이터를 들 수 있다
sealed class ApiState {
object Loading : ApiState()
data class Success(val data: User) : ApiState()
data class Error(val message: String) : ApiState()
}Success 는 data 를 반드시 갖고, Error 는 message 를 반드시 갖는다. data 없는 Success 나 message 없는 Error 는 애초에 만들 수 없다. 상태와 그 상태에 필요한 값이 타입으로 묶인다.
2-3. when 이 exhaustive 해지는 이유
fun render(state: ApiState) = when (state) {
is ApiState.Loading -> showSpinner()
is ApiState.Success -> showData(state.data)
is ApiState.Error -> showError(state.message)
} // else 없이 컴파일된다sealed class 는 하위 타입 전체 목록을 컴파일러가 안다. 모든 하위 클래스가 같은 파일(또는 같은 모듈, 코틀린 버전에 따라 범위가 다르다)에 선언돼야 하기 때문이다. 그래서 when 이 모든 분기를 다 다뤘는지 컴파일러가 검사할 수 있고, else 없이도 컴파일이 통과한다. 반대로 새 상태(ApiState.Empty)를 추가하면, 그 상태를 처리 안 한 when 은 전부 컴파일 에러가 난다. 처리를 빼먹은 곳을 사람이 찾아다닐 필요가 없다.
일반 class 상속으로 같은 걸 흉내 내면, 하위 타입이 어디서든 추가될 수 있으니 컴파일러가 “이게 전부” 라고 보장할 수 없다. when 에 else 가 강제되고, 새 타입을 빼먹어도 조용히 else 분기로 흘러간다. enum 은 반대로 exhaustive 검사는 되지만 상태별 데이터를 못 든다. sealed class 는 둘의 장점만 합친 것이다.
2-4. 실무에서 흔한 모양
네트워크 요청, 폼 검증 결과, 결제 처리 결과처럼 “성공/실패/진행중” 이 있고 각각 다른 정보가 필요한 곳이면 거의 항상 sealed class 가 맞는 선택이다.
sealed class PaymentResult {
data class Approved(val approvalCode: String, val amount: Long) : PaymentResult()
data class Declined(val reasonCode: String) : PaymentResult()
data object NetworkError : PaymentResult()
}Approved 와 Declined 는 data class 로, 데이터가 없는 NetworkError 는 data object(코틀린 1.9 이상, 이전 버전은 object)로 선언하는 조합이 흔하다.
실패를 명시적 상태로 착지시켜야 한다는 발상 자체는 언어를 안 가린다. Turnstile 로더에서도 onload 콜백이 return 으로 조용히 빠지면서 상태 전환 없이 “로딩중” 에 멈춰버린 게 같은 종류의 문제였다. 다만 그쪽은 타입 시스템이 강제해주지 않는 언어라 사람이 모든 경로에서 상태 갱신을 빼먹지 않도록 직접 챙겨야 했다. sealed class 와 exhaustive when 은 그 책임의 일부를 컴파일러로 옮긴다.
3. 마무리
요약
enum은 상태별로 다른 데이터를 들 수 없어 nullable 필드를 상태 개수만큼 늘리게 된다.sealed class하위 타입은 각자 필요한 데이터를 생성자에 강제할 수 있다.- 하위 타입 전체를 컴파일러가 알기 때문에
when이else없이 exhaustive 하게 컴파일된다.- 새 상태를 추가하면 처리를 빼먹은
when이 전부 컴파일 에러가 나서, 빼먹은 곳을 찾을 필요가 없다.- 데이터 없는 상태는
object(또는data object)로 선언한다.