Search

Sealed classes and interfaces

봉인된 클래스와 인터페이스는 클래스 계층 구조의 제어된 상속을 제공한다.
봉인된 클래스와 인터페이스의 모든 직접 하위 클래스는 컴파일 타임에 알려진다.
봉인된 클래스와 인터페이스가 정의된 모듈과 패키지 외부에 다른 하위 클래스가 나타날 수 없다.
봉인된 인터페이스가 있는 모듈이 컴파일되면 새로운 구현을 만들 수 없다.
직접 하위 클래스는 상위 클래스로부터 직접 상속받는 클래스 간접 하위 클래스는 상위 클래스보다 두 수준 이상 낮은 수준을 상속받은 클래스
Java 15에서는 봉인된 클래스는 sealed 키워드를 사용하고 permits 키워드를 통해 제한된 계층 구조를 정의한다.

봉인된 클래스와 when 표현식

봉인된 클래스와 인터페이스를 when 표현식과 결합하면 모든 하위 클래스의 동작을 포괄할 수 있으며 코드에 부정적인 영향을 미치는 새로운 하위 클래스가 생성되지 않도록 할 수 있다.
sealed class Shape { object Circle : Shape() object Square : Shape() object Triangle : Shape() } fun describeShape(shape: Shape): String { return when (shape) { is Shape.Circle -> "This is a circle" is Shape.Square -> "This is a square" is Shape.Triangle -> "This is a triangle" } }
Kotlin
복사

봉인된 클래스를 사용하는 시나리오

1.
제한된 클래스 상속이 필요한 경우
a.
상속 받는 하위 클래스의 집합이 미리 정의되어 있고 컴파일 시에 모두 알려져야 할 때 유용하다.
2.
타입 안전한 설계가 필요한 경우
a.
상태 관리나 복잡한 조건 논리를 처리할 때 안전성과 패턴 매칭이 중요한 프로젝트에서 유용하다.
3.
폐쇄형 API와 함께 작업할 때
a.
서드파티 클라이언트가 의도한 대로 API를 사용하도록 강제하여 견고하고 유지보수 가능한 공개 API를 제공하고자 할 때 유용하다.

선언

봉인된 클래스나 인터페이스를 선언하려면 sealed 키워드를 사용한다.
// Create a sealed interface sealed interface Error // Create a sealed class that implements sealed interface Error sealed class IOError(): Error // Define subclasses that extend sealed class 'IOError' class FileReadError(val file: File): IOError() class DatabaseError(val source: DataSource): IOError() // Create a singleton object implementing the 'Error' sealed interface object RuntimeError : Error
Kotlin
복사
Error 인터페이스는 모든 에러 타입의 최상위 인터페이스이다.
IOError 클래스는 Error 인터페이스를 구현하는 sealed 클래스이다.
FileReadError 클래스는 IOError 클래스를 상속받아 파일 읽기 에러를 나타내는 클래스이다.
DatabaseError 클래스는 IOError 클래스를 상속받아 데이터베이스 관련 에러를 나타내는 클래스이다.
RuntimeError 객체는 Error 인터페이스를 구현하는 싱글턴 객체로 런타임 에러를 나타낸다.
이러한 계층 구조는 라이브러리 외부에서 새로운 에러 타입을 추가하지 못하도록 제한한다.
이 구조를 통해 라이브러리 개발자는 모든 에러 타입을 명확히 알고 처리할 수 있으며 라이브러리 외부에서 임의로 에러 타입을 추가할 수 없게 되어 에러 처리의 일관성을 유지하는데 도움이 된다.

생성자

봉인된 클래스 자체는 항상 추상 클래스이며 결과적으로 직접 인스턴스화 할 수 없다.
그러나 생성자를 포함하거나 상속할 수 있다.
sealed class Error(val message: String) { class NetworkError : Error("Network failure") class DatabaseError : Error("Database cannot be reached") class UnknownError : Error("An unknown error has occurred") } fun main() { val errors = listOf(Error.NetworkError(), Error.DatabaseError(), Error.UnknownError()) errors.forEach { println(it.message) } } // Network failure // Database cannot be reached // An unknown error has occurred
Kotlin
복사
이러한 생성자는 봉인된 클래스 자체의 인스턴스를 만드는 것이 아니라 하위 클래스를 만드는 것이다.
enum class ErrorSeverity { MINOR, MAJOR, CRITICAL } sealed class Error(val severity: ErrorSeverity) { class FileReadError(val file: File): Error(ErrorSeverity.MAJOR) class DatabaseError(val source: DataSource): Error(ErrorSeverity.CRITICAL) object RuntimeError : Error(ErrorSeverity.CRITICAL) // Additional error types can be added here }
Kotlin
복사
봉인된 클래스 내의 enum 클래스를 사용하여 열거형 상태를 표현하고 추가 세부 정보를 제공할 수 있다.
각 열거형 상수는 단일 인스턴스로만 존재하는 반면 봉인된 클래스의 하위 클래스는 여러 인스턴스를 가질 수 있다.
sealed class IOError { // A sealed class constructor has protected visibility by default. It's visible inside this class and its subclasses constructor() { /*...*/ } // Private constructor, visible inside this class only. // Using a private constructor in a sealed class allows for even stricter control over instantiation, enabling specific initialization procedures within the class. private constructor(description: String): this() { /*...*/ } // This will raise an error because public and internal constructors are not allowed in sealed classes // public constructor(code: Int): this() {} }
Kotlin
복사
봉인된 클래스의 생성자는 protected(기본값) 또는 private 가시성을 가질 수 있다.

상속

봉인된 클래스와 인터페이스의 직접 하위 클래스는 동일한 패키지에서 선언되어야 한다.
이들은 최상위 클래스 또는 다른 명명된 클래스, 명명된 인터페이스 또는 명명된 객체의 개수와 관계없이 내부에 중첩될 수 있다.
하위 클래스는 Kotlin의 일반 상속 규칙과 호환되는 한 어떤 가시성도 가질 수 있다.
봉인된 클래스의 하위 클래스는 적절하게 정규화된 이름을 가져야 하며 로컬 또는 익명 객체로 선언될 수 없다.
enum 클래스는 봉인된 클래스나 다른 클래스를 상속할 순 없지만 봉인된 인터페이스를 구현할 순 있다.
sealed interface Error // enum class extending the sealed interface Error enum class ErrorType : Error { FILE_ERROR, DATABASE_ERROR }
Kotlin
복사
sealed interface Error // 같은 패키지 내에서만 확장 가능한 봉인된 클래스 sealed class IOError(): Error // 'CustomError' 클래스는 'Error'를 확장하며, 어디서든지 확장 가능합니다. open class CustomError(): Error
Kotlin
복사
봉인된 클래스의 직접 하위 클래스가 봉인으로 표시되지 않은 경우 수정자가 허용하는 모든 방식으로 확장할 수 있다.

멀티 플랫폼 프로젝트의 상속

expect, actual 수정자가 없는 봉인된 클래스의 직접 하위 클래스는 동일한 소스 세트에 있어야 한다.
// commonMain 소스 세트 expect sealed class CommonError { class NetworkError : CommonError } // 중간 소스 세트 (sharedMain) sealed class SharedError : CommonError() { class SharedNetworkError : SharedError() } // 플랫폼 소스 세트 (androidMain, iosMain) actual sealed class CommonError { actual class NetworkError : CommonError() class AndroidError : CommonError() }
Kotlin
복사
expect로 공통 소스 세트에 봉인된 클래스를 선언하고 각 플랫폼 소스 세트에 actual로 구현할 수 있다.
공통 소스 세트와 플랫폼 소스 세트 사이의 어느 소스 세트에서도 하위 클래스를 생성할 수 있으므로 계층 구조를 만들어 봉인된 클래스를 안전하고 일관되게 사용할 수 있다.
expect : 실제 구현이 다른 플랫폼 소스 세트에 있을 것임을 나타낸다. actual : 각 플랫폼은 expect 선언에 대한 구현을 제공하며 플랫폼별로 다르게 동작할 수 있도록 한다.

when 표현식과 함께 봉인된 클래스 사용

봉인된 클래스를 사용하는 주요 이점은 when 표현식에서 사용할 때 나타난다.
when 표현식은 Kotlin 컴파일러가 모든 가능한 케이스가 포함되었는지 철저히 확인할 수 있도록 한다.
// Function to log errors fun log(e: Error) = when(e) { is Error.FileReadError -> println("Error while reading file ${e.file}") is Error.DatabaseError -> println("Error while reading from database ${e.source}") Error.RuntimeError -> println("Runtime error") // No `else` clause is required because all the cases are covered }
Kotlin
복사
멀티 플랫폼 프로젝트에서 공통 코드에 expect 선언으로 when 표현식이 붙은 봉인된 클래스가 있다면 actual 플랫폼 구현의 하위 클래스가 공통 코드에 알려지지 않은 봉인된 클래스를 확장할 수 있기 때문에 else 브랜치가 필요하다.

사용 사례

UI 어플리케이션의 상태 관리

봉인된 클래스를 사용하여 어플리케이션에서 다양한 UI 상태를 나타낼 수 있다.
이 접근 방식은 UI 변경 사항을 체계적이고 안전하게 처리할 수 있게 한다.
sealed class UIState { data object Loading : UIState() data class Success(val data: String) : UIState() data class Error(val exception: Exception) : UIState() } fun updateUI(state: UIState) { when (state) { is UIState.Loading -> showLoadingIndicator() is UIState.Success -> showData(state.data) is UIState.Error -> showError(state.exception) } }
Kotlin
복사

결제 방법 처리

when 표현식이 있는 봉인된 클래스를 사용하여 다양한 결제 방법을 효율적으로 처리할 수 있다.
다양한 결제 방법을 봉인된 클래스의 하위 클래스로 표현함으로써 트랜잭션 처리를 위한 명확하고 관리하기 쉬운 구조를 확립한다.
sealed class Payment { data class CreditCard(val number: String, val expiryDate: String) : Payment() data class PayPal(val email: String) : Payment() data object Cash : Payment() } fun processPayment(payment: Payment) { when (payment) { is Payment.CreditCard -> processCreditCardPayment(payment.number, payment.expiryDate) is Payment.PayPal -> processPayPalPayment(payment.email) is Payment.Cash -> processCashPayment() } }
Kotlin
복사

API 요청-응답 처리

봉인된 클래스와 봉인된 인터페이스를 사용하여 API 요청과 응답을 처리하는 사용자 인증 시스템을 구현할 수 있다.
// Import necessary modules import io.ktor.server.application.* import io.ktor.server.resources.* import kotlinx.serialization.* // Define the sealed interface for API requests using Ktor resources @Resource("api") sealed interface ApiRequest @Serializable @Resource("login") data class LoginRequest(val username: String, val password: String) : ApiRequest @Serializable @Resource("logout") object LogoutRequest : ApiRequest // Define the ApiResponse sealed class with detailed response types sealed class ApiResponse { data class UserSuccess(val user: UserData) : ApiResponse() data object UserNotFound : ApiResponse() data class Error(val message: String) : ApiResponse() } // User data class to be used in the success response data class UserData(val userId: String, val name: String, val email: String) // Function to validate user credentials (for demonstration purposes) fun isValidUser(username: String, password: String): Boolean { // Some validation logic (this is just a placeholder) return username == "validUser" && password == "validPass" } // Function to handle API requests with detailed responses fun handleRequest(request: ApiRequest): ApiResponse { return when (request) { is LoginRequest -> { if (isValidUser(request.username, request.password)) { ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail")) } else { ApiResponse.Error("Invalid username or password") } } is LogoutRequest -> { // Assuming logout operation always succeeds for this example ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail")) // For demonstration } } } // Function to simulate a getUserById call fun getUserById(userId: String): ApiResponse { return if (userId == "validUserId") { ApiResponse.UserSuccess(UserData("validUserId", "John Doe", "john@example.com")) } else { ApiResponse.UserNotFound } // Error handling would also result in an Error response. } // Main function to demonstrate the usage fun main() { val loginResponse = handleRequest(LoginRequest("user", "pass")) println(loginResponse) val logoutResponse = handleRequest(LogoutRequest) println(logoutResponse) val userResponse = getUserById("validUserId") println(userResponse) val userNotFoundResponse = getUserById("invalidId") println(userNotFoundResponse) }
Kotlin
복사
ApiRequest 봉인된 인터페이스는 LoginRequest 및 LogoutRequest 작업에 대한 특정 요청 유형을 정의한다.
ApiResponse 봉인된 클래스는 UserSuccess, UserNotFount 및 Error에 대한 다양한 응답 시나리오를 캡슐화한다.
handleRequest 함수는 when 표현식을 사용하여 ApiRequest들을 타입 세이프한 방식으로 처리한다.
getUserById 함수는 사용자 데이터를 시뮬레이션한다.