
sealed class vs enum을 코틀린에서 비교할 때 가장 쉬운 방법은 문법 정의보다 상태 예제부터 보는 것입니다. 결론부터 말하면, 상태 이름만 고르면 되는 문제라면 enum class가 단순하고, 상태마다 다른 데이터까지 함께 들고 다녀야 하면 sealed class가 더 자연스럽습니다.
특히 안드로이드나 서버 코드에서 로딩, 성공, 실패 같은 상태를 모델링할 때 이 차이가 바로 드러납니다. 이번 글은 UI 상태 예제로 먼저 감을 잡고, 그다음에 payload, exhaustive when, 그리고 enum이 더 나은 순간까지 차분하게 정리해보겠습니다.
먼저 한 줄 기준만 기억해도 좋습니다. 상태가 이름만 다르면 enum, 상태마다 함께 움직이는 값이 다르면 sealed class가 보통 더 잘 맞습니다.
왜 UI 상태에서 sealed class가 자주 나올까
예를 들어 게시글 목록 화면을 만든다고 해보겠습니다. 처음에는 로딩 중이고, 성공하면 목록 데이터가 필요하고, 실패하면 에러 메시지를 함께 보여줘야 할 수 있습니다. 이런 상태를 enum으로 표현하면 금방 어색해집니다.
enum class ScreenState {
LOADING,
SUCCESS,
ERROR,
}
// SUCCESS일 때 posts는 어디에 둘까?
// ERROR일 때 message는 어디에 둘까?이 코드는 상태 이름은 잘 표현합니다. 그런데 SUCCESS일 때 필요한 posts, ERROR일 때 필요한 message를 자연스럽게 담기가 어렵습니다. 결국 별도 변수 두세 개를 옆에 더 두게 되고, 어떤 값이 어떤 상태와 짝인지 코드가 흩어지기 시작합니다.
sealed class ScreenState {
data object Loading : ScreenState()
data class Success(val posts: List<String>) : ScreenState()
data class Error(val message: String) : ScreenState()
}sealed class로 바꾸면 상태와 데이터가 한 덩어리로 붙습니다. 성공 상태에는 성공에 필요한 값만 들어가고, 실패 상태에는 실패에 필요한 값만 들어갑니다. 이 구조는 UI를 그릴 때 훨씬 읽기 쉽고, 실수도 줄여줍니다.
안드로이드에서 상태를 어디에 둘지 고민하고 있다면 StateFlow와 SharedFlow 차이나 rememberSaveable과 SavedStateHandle 차이 글도 함께 보면 흐름이 더 잘 잡힙니다.
enum이 더 잘 맞는 장면
반대로 모든 상태 모델링에 sealed class가 필요한 것은 아닙니다. 값이 전혀 붙지 않고, 단순히 고정된 선택지 중 하나만 고르면 되는 경우에는 enum이 더 간단하고 더 읽기 좋습니다.
enum class SortOrder {
LATEST,
OLDEST,
POPULAR
}
enum class ThemeMode {
LIGHT,
DARK,
SYSTEM
}정렬 기준, 테마 모드, 사용자 권한, 요일, 탭 종류처럼 이름 자체가 의미의 대부분인 경우에는 enum이 아주 잘 맞습니다. 이럴 때 sealed class를 쓰면 오히려 타입이 늘어나고, 얻는 것보다 보일러플레이트가 많아질 수 있습니다.
즉, enum은 고정된 상수 집합을 표현하는 도구이고, sealed class는 제한된 상태 계층을 표현하는 도구입니다. 둘 다 경우의 수를 닫아두는 점은 비슷하지만, 표현하는 밀도가 다릅니다.
핵심 1: payload를 어디에 둘까
실무에서 가장 큰 차이는 payload입니다. 여기서 payload는 상태와 함께 들고 다니는 실제 데이터라고 보면 됩니다. 로딩 퍼센트, 성공 결과, 실패 이유처럼 상태별로 필요한 값이 다를 때가 바로 여기에 해당합니다.
enum도 값을 가질 수는 있다
Kotlin 공식 문서처럼 enum도 생성자 인자와 프로퍼티를 가질 수 있습니다. 다만 그 값은 enum 전체가 공유하는 공통 구조에 가깝습니다.
enum class ErrorSeverity(val priority: Int) {
MINOR(1),
MAJOR(2),
CRITICAL(3)
}이런 구조는 아주 좋습니다. 모든 상수가 같은 모양의 데이터를 가지기 때문입니다. 하지만 SUCCESS에는 목록이 필요하고, ERROR에는 예외 메시지가 필요하고, EMPTY에는 데이터가 필요 없는 식이라면 enum은 금방 답답해집니다.
sealed class는 상태별 데이터 구조가 달라도 된다
sealed interface ResultState {
data object Idle : ResultState
data object Loading : ResultState
data class Success(val data: List<String>) : ResultState
data class Error(val cause: Throwable, val retryable: Boolean) : ResultState
}여기서는 각 상태가 자기에게 필요한 값만 가집니다. Success는 결과 데이터, Error는 예외와 재시도 가능 여부를 들고 있습니다. 상태와 데이터의 결합이 강해져서, 코드가 상태 중심으로 읽히기 시작합니다.
핵심 2: when 분기에서 뭐가 달라질까
enum과 sealed class는 둘 다 when에서 강합니다. Kotlin 공식 문서 기준으로 when을 expression으로 쓸 때 enum이나 sealed class의 가능한 경우를 모두 처리하면 else 없이도 exhaustive 검사를 받을 수 있습니다.
fun label(order: SortOrder): String = when (order) {
SortOrder.LATEST -> "최신순"
SortOrder.OLDEST -> "오래된순"
SortOrder.POPULAR -> "인기순"
}enum에서는 모든 상수를 다 처리했는지가 중요합니다. 새 상수를 추가하면 이 when도 함께 점검해야 해서 변경 누락을 줄이는 데 도움이 됩니다.
fun render(state: ScreenState): String = when (state) {
ScreenState.Loading -> "로딩 중"
is ScreenState.Success -> "게시글 ${'$'}{state.posts.size}개"
is ScreenState.Error -> state.message
}sealed class에서는 단순히 경우의 수를 다루는 것에서 한 단계 더 나아갑니다. 각 분기 안에서 smart cast가 함께 일어나기 때문에, 해당 상태가 가진 데이터를 바로 꺼내 쓸 수 있습니다. 그래서 상태 분기와 데이터 접근이 하나의 문맥으로 묶입니다.
상태별로 필요한 데이터 접근까지 함께 처리해야 한다면 sealed class의 exhaustive when이 더 강하게 체감됩니다.
왜 UI 상태에서는 더 자연스러울까
UI 상태는 보통 화면에 보여줄 값과 함께 움직입니다. 로딩 중이면 스피너를 보여주고, 성공이면 리스트를 그리고, 빈 상태면 안내 문구를 보여주고, 실패면 에러 메시지와 재시도 버튼을 보여줍니다. 이때 상태와 데이터가 분리되어 있으면 뷰 코드가 자주 꼬입니다.
sealed interface UserListUiState {
data object Loading : UserListUiState
data object Empty : UserListUiState
data class Success(val users: List<String>) : UserListUiState
data class Error(val message: String) : UserListUiState
}
fun render(state: UserListUiState) {
when (state) {
UserListUiState.Loading -> showLoading()
UserListUiState.Empty -> showEmptyGuide()
is UserListUiState.Success -> showUsers(state.users)
is UserListUiState.Error -> showError(state.message)
}
}이 구조가 좋은 이유는 화면이 가능한 상태를 스스로 설명하기 때문입니다. Success인데 사용자 목록이 비어 있다거나, Error인데 메시지가 없는 식의 어색한 조합을 줄이기 쉽습니다.
만약 Compose 상태 관리 감각을 더 보고 싶다면 remember와 rememberSaveable 차이 글도 같이 읽어보면 좋습니다. 상태를 어떻게 보존할지와 상태를 어떻게 모델링할지는 서로 연결된 문제이기 때문입니다.
항상 정답은 아니다
sealed class가 강력한 것은 맞지만, 작은 문제를 너무 무겁게 만들 수도 있습니다. 아래처럼 단순한 탭 선택 상태까지 모두 sealed class로 만들면 구조가 과해질 수 있습니다.
sealed class TabSelection {
data object Home : TabSelection()
data object Search : TabSelection()
data object Profile : TabSelection()
}이 경우는 enum class TabSelection { HOME, SEARCH, PROFILE } 쪽이 훨씬 단순합니다. 분기할 때 필요한 추가 데이터가 없고, 이름만으로 충분하기 때문입니다.
- 고정된 선택지인가?
- 각 상태가 같은 모양의 정보만 가지는가?
- 상태별 개별 payload가 필요 없는가?
- 문제의 핵심이 이름 구분 자체인가?
위 질문에 대부분 예라면 enum부터 검토해도 됩니다. 설계는 강한 도구를 쓰는 것보다, 문제 크기에 맞는 도구를 고르는 쪽이 더 중요합니다.
실무 선택 기준
- 상태 이름만 필요하면 enum을 먼저 본다.
- 상태마다 필요한 데이터가 다르면 sealed class를 먼저 본다.
- when 분기 안에서 상태별 데이터 접근이 많으면 sealed class 쪽이 읽기 쉽다.
- 정렬, 모드, 탭, 권한처럼 닫힌 상수 집합이면 enum이 단순하다.
- UI state, network result, 결제 방식처럼 상태별 의미와 데이터가 함께 바뀌면 sealed class가 자연스럽다.
한 문장으로 줄이면 이렇습니다. enum은 고정된 선택지를 표현할 때, sealed class는 상태별 행동과 데이터를 함께 묶어야 할 때 더 빛납니다.
함께 보면 좋은 글
코틀린 값 모델링 관점은 코틀린 data class: 실무에서 어디까지 쓸까와 같이 보면 더 잘 이어집니다. 안드로이드 상태 처리 문맥은 StateFlow와 SharedFlow 차이도 함께 참고해보세요.
공식 참고 자료는 Kotlin enum classes 문서, Kotlin sealed classes and interfaces 문서, Kotlin when expression 문서입니다.
정리
코틀린에서 enum과 sealed class는 둘 다 닫힌 경우의 수를 다루는 데 강한 도구입니다. 하지만 질문이 조금 다릅니다. enum은 “선택지가 몇 개인가”에 가깝고, sealed class는 “각 상태가 어떤 의미와 데이터를 가지는가”에 더 가깝습니다.
그래서 상태 모델링에서는 문법 암기보다 데이터 흐름을 먼저 보면 판단이 쉬워집니다. 상태 이름만 있으면 enum, 상태마다 다른 데이터와 분기가 함께 움직이면 sealed class. 이 기준만 잡아도 코드가 훨씬 덜 헷갈립니다.