•
봉인된 클래스와 인터페이스는 클래스 계층 구조의 제어된 상속을 제공한다.
•
봉인된 클래스와 인터페이스의 모든 직접 하위 클래스는 컴파일 타임에 알려진다.
•
봉인된 클래스와 인터페이스가 정의된 모듈과 패키지 외부에 다른 하위 클래스가 나타날 수 없다.
•
봉인된 인터페이스가 있는 모듈이 컴파일되면 새로운 구현을 만들 수 없다.
직접 하위 클래스는 상위 클래스로부터 직접 상속받는 클래스
간접 하위 클래스는 상위 클래스보다 두 수준 이상 낮은 수준을 상속받은 클래스
•
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 함수는 사용자 데이터를 시뮬레이션한다.