(Kotlin/코틀린) Kotlin sealed interface

개요


1. 왜 필요한가

// sealed class — 단일 상속만 가능
sealed class NetworkResult<out T> {
    data class Success<T>(val data: T) : NetworkResult<T>()
    data class Error(val message: String) : NetworkResult<Nothing>()
    object Loading : NetworkResult<Nothing>()
}

// 만약 Success가 다른 클래스도 상속받아야 한다면?
// data class Success<T>(val data: T) : NetworkResult<T>(), Cacheable()  // ❌ 다중 상속 불가
// sealed interface — 다른 클래스/인터페이스를 함께 구현 가능
sealed interface NetworkResult<out T>

data class Success<T>(val data: T) : NetworkResult<T>, Cacheable
data class Error(val message: String) : NetworkResult<Nothing>, Loggable
object Loading : NetworkResult<Nothing>

interface Cacheable { fun cacheKey(): String = "default" }
interface Loggable { fun log(): String = "error" }

2. sealed class vs sealed interface

// sealed class — 생성자, 프로퍼티(상태)를 가질 수 있음
sealed class UiState {
    abstract val isLoading: Boolean

    data class Content(override val isLoading: Boolean, val data: List<String>) : UiState()
    data class Error(override val isLoading: Boolean, val message: String) : UiState()
}
// sealed interface — 생성자 없음, 다중 구현 가능
sealed interface UiState {
    data class Content(val data: List<String>) : UiState
    data class Error(val message: String) : UiState
    object Loading : UiState
}

// 다른 인터페이스와 함께 구현 가능
interface Parcelable
data class ContentWithParcel(val data: List<String>) : UiState, Parcelable
항목 sealed class sealed interface
생성자 ✅ 가능 (공통 프로퍼티 보관) ❌ 불가
다중 구현 ❌ 단일 상속만 ✅ 여러 인터페이스 동시 구현
프로퍼티 상태 ✅ 가능 abstract val만 선언 가능
when 완전성 검사 ✅ 동일하게 지원 ✅ 동일하게 지원
주 사용처 공통 상태를 가진 계층 분류만 필요한 마커 역할

3. when 완전성 검사 — 공통점

sealed interface ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>
    data class Error(val code: Int, val message: String) : ApiResult<Nothing>
    object Loading : ApiResult<Nothing>
}

fun handle(result: ApiResult<String>) {
    // else 없이 모든 분기 처리 — 컴파일러가 강제
    when (result) {
        is ApiResult.Success -> println(result.data)
        is ApiResult.Error   -> println("에러: ${result.message}")
        ApiResult.Loading    -> println("로딩 중")
    }
}

// 새 구현체를 추가하면 when에서 컴파일 에러 발생 → 누락 방지

4. 다중 분류가 필요한 경우

// 여러 sealed interface를 동시에 구현해 "다중 분류" 표현
sealed interface Shape
sealed interface Drawable

data class Circle(val radius: Double) : Shape, Drawable
data class Rectangle(val width: Double, val height: Double) : Shape, Drawable
object Point : Shape  // Drawable은 아님

fun describe(item: Shape) {
    when (item) {
        is Circle    -> println("원, 반지름 ${item.radius}")
        is Rectangle -> println("사각형 ${item.width}x${item.height}")
        Point        -> println("점")
    }
}

fun render(item: Drawable) {
    println("렌더링: $item")
}

5. Android 실전 예제

권한 상태 — 다른 인터페이스와 결합

interface Loggable {
    fun logTag(): String
}

sealed interface PermissionState {
    data class Granted(val permission: String) : PermissionState, Loggable {
        override fun logTag() = "PERMISSION_GRANTED"
    }
    data class Denied(val permission: String, val shouldShowRationale: Boolean) :
        PermissionState, Loggable {
        override fun logTag() = "PERMISSION_DENIED"
    }
    object NotRequested : PermissionState
}

fun handlePermission(state: PermissionState) {
    when (state) {
        is PermissionState.Granted -> startCamera()
        is PermissionState.Denied  ->
            if (state.shouldShowRationale) showRationale() else openSettings()
        PermissionState.NotRequested -> requestPermission()
    }
}
sealed interface Screen {
    val route: String

    object Home : Screen { override val route = "home" }
    data class Detail(val itemId: String) : Screen {
        override val route = "detail/$itemId"
    }
    data class Profile(val userId: String) : Screen {
        override val route = "profile/$userId"
    }
}

fun navigate(screen: Screen) {
    navController.navigate(screen.route)
}

MVI Intent/Effect 분리에 sealed interface 활용

sealed interface HomeIntent {
    object LoadData : HomeIntent
    data class Search(val query: String) : HomeIntent
    data class DeleteItem(val id: Long) : HomeIntent
}

sealed interface HomeEffect {
    data class ShowSnackbar(val message: String) : HomeEffect
    data class NavigateToDetail(val id: Long) : HomeEffect
}

fun reduce(intent: HomeIntent): HomeEffect? = when (intent) {
    HomeIntent.LoadData -> null
    is HomeIntent.Search -> null
    is HomeIntent.DeleteItem -> HomeEffect.ShowSnackbar("삭제됨")
}

6. 선택 기준

// ✅ 공통 프로퍼티/생성자가 필요하면 sealed class
sealed class FormField(open val isValid: Boolean) {
    data class Email(val value: String, override val isValid: Boolean) : FormField(isValid)
}

// ✅ 단순 분류 + 다중 구현이 필요하면 sealed interface
sealed interface ValidationResult
object Valid : ValidationResult
data class Invalid(val reason: String) : ValidationResult
상황 선택
공통 상태(프로퍼티)를 모든 하위 타입이 가져야 함 sealed class
하위 타입이 다른 클래스/인터페이스도 함께 구현해야 함 sealed interface
단순히 “이 중 하나”라는 분류만 필요 sealed interface (더 가벼움)

7. 정리

항목 내용
sealed interface 제한된 구현체 집합을 갖는 인터페이스, 다중 구현 가능
sealed class와 차이 생성자 없음 / 다중 상속(구현) 가능
when 완전성 sealed class와 동일하게 컴파일러가 분기 누락을 검사
Android 활용 권한 상태, Navigation 라우트, MVI Intent/Effect 분류

참고



Related Posts