본문 바로가기

안드로이드/코틀린

[안드로이드 코틀린 Sealed class] 특성 및 코드 예제 #2


안드로이드 코틀린 Sealed Class의 3가지 특성

안녕하세요. 지난 포스팅에서는 Kotlin의 enum class 및 sealed class의 사용 이유에 대해서 살펴보았습니다. 오늘은 sealed class의 특성을 제한된 클래스 계층, 타입 안전성, 그리고 서브 클래스 타입 차이 (class, data class, object) 세 가지 기준으로 설명하겠습니다.

1. 제한된 클래스 계층

sealed class는 Kotlin에서 제한된 클래스 계층을 구성하기 위해 사용되는 특별한 종류의 클래스입니다. sealed class의 가장 큰 특징은 그 하위 클래스가 반드시 sealed class와 같은 파일 내에서 선언되어야 한다는 것입니다. 컴파일러는 sealed class와 같은 파일 내에 선언된 하위 클래스들만을 검사하므로, 모든 케이스가 명확하게 처리됩니다. 이러한 설계는 when 표현식에서 모든 가능한 케이스를 처리할 수 있도록 하며, else 케이스를 추가하지 않아도 되게 만듭니다.

즉, sealed class는 추상 클래스로써 이를 상속받는 서브 클래스는 반드시 해당 sealed class와 같은 파일 내에서만 정의되어야 합니다. 다른 파일에서 sealed class를 상속받는다면 컴파일 에러가 발생하게 됩니다.

다른 파일에서 sealed class를 상속 받았을 때 에러가 발생한 모습

러한 강력한 제한 덕분에 when 절에서 else 케이스를 추가하지 않아도 되고, 개발자가 실수 없이 모든 케이스를 처리할 수 있게 됩니다. 컴파일러는 sealed class와 같은 파일에 있는 서브 클래스들만 확인하면 되기 때문에 서브 클래스의 종류가 명확해지기 때문입니다.

sealed class ApiResponse {
    data class Success(val data: String) : ApiResponse()
    data class Error(val error: String) : ApiResponse()
    object Loading : ApiResponse()
}

fun handleResponse(response: ApiResponse) {
    when (response) {
        is ApiResponse.Success -> println("Success with data: ${response.data}")
        is ApiResponse.Error -> println("Error: ${response.error}")
        is ApiResponse.Loading -> println("Loading...")
        // 'else' 케이스가 필요 없음
    }
}

2. 타입 안전성

새로운 서브 클래스가 추가될 때도 안전합니다. when 조건문을 사용하는 코드에서 컴파일 타임에 모든 서브 클래스를 처리하도록 강제하기 때문에 관련된 처리 로직을 누락하는 것을 방지하게 됩니다.

sealed class ApiResponse {
    data class Success(val data: String) : ApiResponse()
    data class Error(val error: String) : ApiResponse()
    object Loading : ApiResponse()
    data class NoData : ApiResponse() // 새로운 하위 클래스 추가
}

만약 새로운 서브 클래스에 대한 로직을 처리하지 않았다면 컴파일 타임에 에러가 발생하게 됩니다. 예를 들어, ApiResponse에 NoData라는 새로운 서브 클래스를 추가했을 경우, 기존의 when 조건문에서 NoData를 처리하지 않으면 컴파일 에러가 발생합니다. 이를 통해 개발자는 모든 케이스를 명확하게 처리하도록 유도됩니다.

새로운 서브 클래스가 추가됐을 때 발생하는 에러
NoData 서브 클래스가 추가됐기 때문에 에러가 발생한 모습

즉, 개발자는 이러한 에러를 컴파일 타임에 해결할 수 있게 되고 런타임 에러를 피할 수 있게 됩니다.

fun handleResponse(response: ApiResponse) {
    when (response) {
        is ApiResponse.Success -> println("Success with data: ${response.data}")
        is ApiResponse.Error -> println("Error: ${response.error}")
        is ApiResponse.Loading -> println("Loading...")
        is ApiResponse.NoData -> println("No data available") // 'NoData' 케이스를 추가
    }
}

3. 하위 클래스 타입차이 (class, data class, object)

sealed class의 서브 클래스는 class, data class, 또는 object로 선언할 수 있으며, 각 타입은 다른 목적을 가집니다.

  • class와 data class
    하위 클래스가 상태 변수를 가지고 있을 경우, class 또는 data class로 선언됩니다. data class는 데이터의 저장과 처리에 중점을 두며, class는 보다 복잡한 로직과 상태 관리에 적합합니다.
  • object: object
    싱글턴 인스턴스를 생성하므로, 상태 변수가 없는 경우에 사용됩니다. object를 사용하면 객체 생성 비용을 절약하고, 메모리 사용을 최적화할 수 있습니다.

이러한 차이는 sealed class를 사용하여 다양한 타입의 하위 클래스를 유연하게 정의할 수 있게 해 줍니다. 예를 들어, ApiResponse에서 Success와 Error는 데이터를 담는 data class로, Loading은 상태를 나타내는데 충분한 object로 선언됩니다. 서브 클래스의 기능을 고려해서 적합한 타입을 선언하면 됩니다.

sealed class ApiResponse {
    data class Success(val data: String) : ApiResponse() // data class 선언
    data class Error(val error: String) : ApiResponse() // data class 선언
    object Loading : ApiResponse() // object 선언
}

안드로이드 코틀린의 Sealed Class 정리

sealed class는 제한된 클래스 계층을 통해 런타임 오류를 줄이고, 타입 안전성을 높이며, 다양한 타입의 하위 클래스를 유연하게 정의할 수 있습니다. enum class의 한계를 넘어서서, 각 상태별로 다른 데이터 타입이나 추가적인 정보를 필요로 하는 경우에 sealed class를 활용할 수 있습니다. 따라서, 구현하려는 기능에 가장 적합한 방법을 선택하여 사용하면, 개발 과정에서의 유지보수성과 가독성이 크게 향상될 것입니다.