Sealed Class는 상속 계층 구조를 고정된 서브타입 집합으로 제한하는 Kotlin의 특별한 클래스 유형입니다. 모든 서브클래스는 동일한 파일에 선언되며 컴파일러가 이를 알고 있어, 필수 else 분기 없이 완전한 when 블록을 사용할 수 있습니다. Kotlin Docs, 2026에 따르면, 봉인된 클래스는 상태, 오류 유형 및 UI 이벤트와 같은 제한된 계층 구조를 표현하기 위한 핵심 메커니즘입니다.
핵심 포인트
Sealed Class(봉인된 클래스)는 sealed 수정자로 표시된 Kotlin의 클래스입니다. 제한된 타입 계층 구조를 정의하며, 가능한 모든 서브클래스가 동일한 파일에 나열되고 컴파일러가 각각을 알고 있습니다. 이는 서브클래스가 어디서든 선언될 수 있는 일반적인 오픈 클래스와 sealed class를 구분합니다.
Sealed class의 주요 목적은 유한한 변형 집합의 타입 안전한 표현입니다. 각 서브클래스는 고유한 데이터 구조를 가질 수 있어 sealed class가 enum보다 더 유연합니다. 런타임에 sealed class는 일반적인 추상 클래스이며, 컴파일러는 컴파일 시간에만 제약을 적용합니다.
Sealed class는 Android 앱 아키텍처에서 특히 유용합니다: UI 상태, 네트워크 요청 결과, Intent와 유사한 탐색 이벤트, 그리고 물론 오류 계층 구조가 일반적인 사용 사례입니다.
컴파일 시간에 sealed class는 when 표현식을 위한 점프 테이블로 최적화되어 if-else 체인보다 더 효율적입니다. data class와 결합하면 각 서브클래스는 상태뿐만 아니라 메서드도 포함할 수 있어, 보일러플레이트 코드 없이 자체 문서화되는 도메인 모델을 구축할 수 있습니다.
Sealed class는 모바일 애플리케이션에서 상태 기계를 표현하는 데도 효과적입니다. 각 상태는 고유한 매개변수를 가진 별도의 서브클래스이며, 상태 간 전환은 when 표현식을 통해 제어됩니다. 컴파일러는 모든 가능한 상태가 처리됨을 보장하여, UI 상태나 비즈니스 로직을 변경할 때 런타임 오류를 제거합니다.
초보 Kotlin 개발자는 종종 sealed class와 enum을 혼동합니다. 둘 다 값의 집합을 제한하기 때문입니다. 그러나 근본적인 차이가 있습니다: enum은 동일한 타입의 상수 집합인 반면, sealed class는 다른 타입들의 계층 구조입니다.
Enum은 모든 변형이 추가 구조 없이 상수일 때 최적입니다. 예를 들어, 요일, 주문 상태 또는 매개변수 없는 작업 유형 등입니다. 각 enum 값은 고정된 이름을 가진 싱글톤입니다.
Sealed class는 각 변형이 고유한 데이터를 가질 때 필요합니다. 예를 들어, 네트워크 오류는 응답 코드를, 구문 분석 오류는 세부 정보를, 인증 오류는 메시지를 포함합니다. sealed class의 각 서브클래스는 고유한 필드를 가진 별도의 타입입니다.
// Enum — 동일한 타입의 모든 변형
enum class Status { LOADING, SUCCESS, ERROR }
// Sealed class — 각 변형이 고유한 데이터를 가짐
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
Kotlin 1.5부터 sealed interface를 선언할 수 있습니다. 이는 봉인 개념을 인터페이스로 확장하며, sealed interface도 고정된 구현 집합을 가지지만 다중 상속을 지원합니다.
Sealed interface는 서브클래스가 여러 계약을 동시에 구현해야 할 때 편리합니다. 예를 들어, UI 이벤트는 클릭 가능하면서 추적 가능할 수 있습니다. sealed class를 사용하면 하나의 기본 클래스를 선택해야 하지만, sealed interface를 사용하면 서브클래스가 둘 다 구현합니다.
Sealed class는 클래스이므로 각 서브클래스는 하나의 부모만 가질 수 있습니다. Sealed interface는 이 문제를 해결하지만 상태를 보유할 수 없습니다. 둘 사이의 선택은 작업에 따라 다릅니다: 필드와 함께 공유 로직이 필요하면 sealed class를, 계약 유연성이 필요하면 sealed interface를 사용하세요.
sealed interface ScreenEvent {
data class Refresh(val force: Boolean) : ScreenEvent
data class Navigate(val route: String) : ScreenEvent
data class ShowError(val toast: String) : ScreenEvent
}
sealed interface AnalyticsEvent {
val name: String
val params: Map<String, Any>
}
// 서브클래스가 두 인터페이스를 모두 구현함
data class LoginClicked(
override val name: String = "login_click",
override val params: Map<String, Any> = emptyMap()
) : ScreenEvent, AnalyticsEvent
모바일 개발에서 sealed class의 주요 용도 중 하나는 타입 안전한 오류 계층 구조입니다. 다른 유형의 예외를 던지거나 일반 Exception을 사용하는 대신, sealed class는 가능한 모든 도메인 오류를 단일 타입으로 수집합니다.
DomainError라는 sealed class를 만들고 모든 실패 유형을 서브클래스로 나열하세요. 각 서브클래스에는 해당 특정 오류 유형과 관련된 데이터만 포함됩니다. 컴파일러는 오류를 처리할 때 어떤 변형도 잊지 않음을 보장합니다.
인증이 있는 앱에서 다양한 실패 시나리오가 가능합니다: 잘못된 비밀번호, 계정 차단, 서버 문제. Sealed class는 이를 완전한 처리와 함께 단일 타입으로 결합합니다.
sealed class AuthError {
data class InvalidCredentials(
val attempts: Int
) : AuthError()
data class AccountBlocked(
val until: Long
) : AuthError()
data class NetworkFailure(
val cause: Throwable
) : AuthError()
object ServerError : AuthError()
}
fun handleError(error: AuthError): String = when (error) {
is AuthError.InvalidCredentials ->
"남은 시도 횟수: ${3 - error.attempts}"
is AuthError.AccountBlocked ->
"${Date(error.until)}까지 액세스 차단됨"
is AuthError.NetworkFailure ->
"연결 확인: ${error.cause.localizedMessage}"
AuthError.ServerError ->
"서버를 일시적으로 사용할 수 없음"
}
Sealed class는 Android 애플리케이션 아키텍처에서 표준 도구가 되었습니다. 모바일 개발에서 sealed class가 필수적인 세 가지 주요 패턴을 살펴보겠습니다.
Clean Architecture에서 sealed class의 사용도 주목할 만합니다. 각 계층(data, domain, presentation)은 오류 유형에 sealed class를 사용하고, 매퍼가 한 sealed class를 다른 sealed class로 변환합니다. 예를 들어, 데이터 계층의 DataError는 비즈니스 로직을 위해 DomainError로 매핑된 다음 프레젠테이션 계층을 위해 UiState로 매핑됩니다. 이는 애플리케이션의 모든 수준에서 타입 안전성을 유지하며 어떤 오류도 처리되지 않은 상태로 남지 않음을 보장합니다.
테스트는 각 서브클래스가 고유한 상태를 가진 별도의 타입이므로 sealed class에 특별한 접근 방식이 필요합니다. sealed class의 모든 서브클래스를 반복하는 매개변수화된 테스트를 작성하는 것이 좋습니다. 이는 when 표현식이 계층 구조를 확장할 때 추가된 새로운 것을 포함한 모든 변형을 포괄함을 보장합니다.
UI 테스트의 경우 sealed class를 UiState로 사용하면 각 상태의 표시를 확인할 수 있습니다: Loading은 스피너를, Content는 데이터를, Error는 오류 메시지를 표시합니다. sealed class는 유한하므로 모든 상태의 테스트 커버리지는 UI 로직의 정확성에 완전한 신뢰를 제공합니다.
개념의 단순함에도 불구하고 개발자는 sealed class 계층 구조를 설계할 때 정기적으로 실수를 합니다. 주요 문제와 이를 피하는 방법을 살펴보겠습니다.
자주 묻는 질문
네, sealed class에는 추상 메서드가 포함될 수 있으며, 각 서브클래스는 이를 구현해야 합니다. 이는 모든 변형이 공통 인터페이스를 제공해야 하지만 실행 로직이 다른 경우에 편리합니다.
Java 17+에서 sealed 수정자를 가진 봉인된 클래스와 인터페이스가 도입되었습니다. Android는 현재 Java 17을 부분적으로 지원하지만, Kotlin 프로젝트에서는 sealed class가 Kotlin 1.0부터 제한 없이 사용 가능합니다.
네, 한 sealed class가 다른 sealed class의 서브클래스가 될 수 있습니다. sealed class 계층 구조는 유한하게 유지됩니다: 컴파일러는 각 수준에서 모든 서브클래스를 알고 있습니다. 이를 통해 상세한 오류 분류를 구축할 수 있습니다.
Sealed class는 런타임 오버헤드를 만들지 않습니다. 컴파일러는 sealed class를 사용한 when 표현식을 점프 테이블(tableswitch)로 최적화하여 if-else 체인보다 빠릅니다. 성능은 enum과 동일합니다.
Sealed class의 각 서브클래스는 별도로 테스트됩니다. sealed class는 유한하므로 모든 변형을 반복하는 매개변수화된 테스트를 작성할 수 있습니다. 이는 when 블록 분기의 완전한 커버리지를 제공합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.