본문 바로가기
Kotlin

[Kotlin] Sealed Class란?

by 태크민 2024. 11. 2.

Enum Class 이야기

enum은 C언어에도 존재할 만큼, 범용성이 뛰어난 녀석입니다. 코틀린에도 당연하게 enum 클래스가 존재합니다. 

다음은 enum 클래스를 활용한 코드 예시입니다.

enum class Color(val r: Int, val g: Int, val b: Int) {
    RED(255, 0, 0), ORANGE(255, 165, 0),
    YELLOW(255, 255, 0), GREEN(0, 255, 0),
    BLUE(0, 0, 255);

    fun rgb() = (r * 256 + g) * 256 + b
}

fun getColorName(color: Color) = when (color) {
    Color.RED -> "빨강"
    Color.ORANGE -> "주황"
    Color.YELLOW -> "노랑"
    Color.GREEN -> "초록"
    Color.BLUE -> "파랑"
}

fun main() {
    println(getColorName(Color.BLUE))
}

 

getColorName() 을 통해서 when 문을 통해 enum 객체 각각에 대한 분기 동작을 정의해주었습니다. 위 예제는 각 색상의 RGB 값을 기반으로 enum 클래스를 만들고, when 을 통해 각각의 한글 이름을 출력하는 동작을 구현해본 것입니다.

 

하지만, 갑자기 런타임 중에 RGB 값이 조금 수정되어야 하는 상황이 발생한다면?

예를들어, enum 객체들 각각에 B (Blue) 값을 20씩 추가하려고 한다면 가능할까요?

 

정답은 NO입니다.

enum 클래스의 각 상수들은 싱글톤 디자인 패턴을 따르기 때문에, 단 하나의 인스턴스만 존재하게 됩니다. 그리고 정적인 상태를 갖기 때문에, 최초 설정한 enum 각각에 대한 상태를 변경할 수 없습니다. 즉, RED(255, 0, 0) 라고 RED 를 정의해뒀으면, 속성값(상태)를 더 이상 바꿀 수 없다는 이야기이죠. 

또한, 상속이 불가능한 형태기 때문에 enum 에 대한 서브 클래스를 생성할 수도 없습니다.

 

이 내용들을 정리하자면 다음과 같습니다.

제약사항

  •  enum 상수들은 단 하나의 인스턴스만 가질 수 있음 (싱글톤)
    • enum 상수 정의 이후에 속성값을 변경할 수 없음
  • enum 클래스에 대해 서브 클래스를 생성할 수 없음

 

하지만, 아예 해결 방법이 없는 것은 아닙니다.

코틀린에서는 이러한 enum 클래스의 제약사항들을 커버할 수 있는 sealed 클래스라는 것을 제공합니다.

 


Sealed Class

sealed 클래스는 기본적으로 자기 자신이 abstract 클래스이고, 자신을 상속받는 여러 서브 클래스들을 가질 수 있습니다. 이를 사용하면 enum 클래스와 달리 상속을 지원하기 때문에, 상속을 활용한 풍부한 동작을 구현할 수 있습니다.

그리고 자신을 상속받는 서브 클래스의 종류를 제한할 수 있습니다. 왜냐하면 sealed 클래스는 다음과 같은 특성을 지니기 때문입니다.

 

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와 Enum Class와 의 차이는 무엇인가?

sealed class와 enum class는 모두 컴파일 타임에 타입 안전성을 제공하고 when 구문에서 모든 가능한 값을 처리할 수 있는 공통점이 있지만,

두 클래스 간의 가장 큰 차이는 enum class는 실제 상수를 처리하고 sealed class에서는 개별 인스턴스를 처리한다는 것입니다.

 

1. 인스턴스 관리

  • enum class:
    • Enum에서는 특정 값을 single instance로서 하나의 객체만 제한적으로 사용할 수 있으며, 생성자의 형태도 동일해야만 합니다.
    • 상태를 변경할 수 없으며, 생성자는 고정되어 있습니다. 따라서 초기화된 후에는 값을 수정할 수 없습니다.
  • sealed class:
    • 여러 개의 하위 클래스 인스턴스를 가질 수 있으며, 각 인스턴스는 고유한 상태를 가질 수 있습니다. 예를 들어, Success와 Error와 같은 다양한 상태를 가질 수 있습니다.
    • 각 하위 클래스는 서로 다른 생성자를 가질 수 있으며, 상태를 포함하여 다양한 속성을 정의할 수 있습니다.

2. 상속 및 구조

  • enum class:
    • 상속을 지원하지 않으며, 정의된 각 상수는 독립적인 인스턴스입니다. 따라서 enum class는 계층 구조를 만들 수 없습니다.
  • sealed class:
    • sealed class는 추상 클래스 역할을 하며, 자신을 상속받는 여러 하위 클래스를 정의할 수 있습니다. 이는 다양한 동작을 구현할 수 있는 유연성을 제공합니다.
    • 하위 클래스는 class, data class, object class로 정의할 수 있어, 복잡한 데이터 구조를 다룰 수 있습니다.

Enum Class와 Sealed Class는 각각 언제 사용하면 좋을까?

  • enum class:
    • 고정된 값의 집합이 필요할 때 사용되며, 주로 상태나 명령을 나타낼 때 적합합니다. 예를 들어, 방향이나 상태 코드와 같이 변하지 않는 상수를 사용할 때 유용합니다.
  • sealed class:
    • 다양한 상태와 행동을 가진 데이터 모델이 필요할 때 사용됩니다. when 표현식에서 각 상태에 대한 처리 로직을 명확하게 정의할 수 있어, 복잡한 비즈니스 로직을 처리하는 데 적합합니다.


그렇다면 Sealed Class와 Data Class의 차이는?

Kotlin에서 data class와 sealed class는 각각 특정한 목적을 가진 클래스 유형입니다. 

  • data class: 데이터를 표현하고, 데이터의 속성을 기반으로 자동으로 메서드를 생성하여 간편하게 처리할 수 있도록 합니다.
  • sealed class: 특정 타입 계층을 정의하고, 해당 타입의 하위 클래스를 제한하여 안전한 타입 검사를 가능하게 합니다.

1. Data Class

  • 정의: data class는 데이터를 저장하기 위해 특별히 설계된 클래스입니다. 주로 간단한 데이터 구조를 표현하고, 자동으로 toString(), equals(), hashCode(), copy() 메서드를 생성합니다.
  • 특징:
    • 최소한 하나 이상의 속성 (프로퍼티)을 가지고 있어야 합니다.
    • 주 생성자에서 val 또는 var로 정의된 프로퍼티를 포함해야 합니다.
    • 일반적으로 데이터의 표현과 관련된 기능에 집중합니다.

2. Sealed Class

  • 정의: sealed class는 특정 타입 계층을 제한하여 다른 클래스에서 상속될 수 있는 클래스를 정의할 때 사용됩니다. sealed class를 사용하면 이 클래스의 하위 클래스 목록을 미리 알고 있으며, 컴파일 타임에 검증할 수 있습니다.
  • 특징:
    • 직접 인스턴스를 생성할 수 없고, 반드시 하위 클래스에서만 인스턴스화해야 합니다.
    • 하위 클래스는 동일한 패키지 내에서 정의되어야 합니다.
    • 주로 when 표현식과 함께 사용하여 모든 가능성을 처리할 수 있도록 합니다


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

sealed class는 제한된 클래스 계층을 통해 런타임 오류를 줄이고, 타입 안전성을 높이며, 다양한 타입의 하위 클래스를 유연하게 정의할 수 있습니다. enum class의 한계를 넘어서서, 서브 클래스 각각에 대해 여러개의 인스턴스가 생성이 가능하며, 상태값을 유동적으로 변경할 수 있습니다. 따라서, 각 상태별로 다른 데이터 타입이나 추가적인 정보를 필요로 하는 경우에 sealed class를 활용할 수 있습니다.

구현하려는 기능에 가장 적합한 방법을 선택하여 사용하면, 개발 과정에서의 유지보수성과 가독성이 크게 향상될 것입니다. 

 

Sealed Class 는 Enum Class 의 확장판과도 같습니다. 제한적인 계층관계를 효과적으로 표현할 수 있고, 이에 따라 when 문 사용 시 효과적으로 사용할 수 있습니다.

 

 

끝.


참고자료

https://velog.io/@haero_kim/Kotlin-Sealed-Class-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0

 

[Kotlin] Sealed Class 알아보기

Enum Class 의 확장판, Sealed Class 의 개념

velog.io

https://dev-inventory.com/15

 

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

안드로이드 코틀린 Sealed Class의 3가지 특성 안녕하세요. 지난 포스팅에서는 Kotlin의 enum class 및 sealed class의 사용 이유에 대해서 살펴보았습니다. 오늘은 sealed class의 특성을 제한된 클래스 계층,

dev-inventory.com

https://velog.io/@ho-taek/Kotlin-Sealed-Class%EB%9E%80

 

[Kotlin] Sealed Class란

! 프로젝트 진행하면서 코드를 좀더 간결하게 짜고 싶은 마음에 다시 코틀린 공부를 시작하고 있다.(미리미리좀 해놓을걸...) 앱잼도 끝났고 릴리즈 및 버전 업에도 쓰일만한 것들을 위주로 공부

velog.io