이동식 저장소

Sealed class vs. enum class 본문

Primary/Kotlin

Sealed class vs. enum class

해스끼 2024. 6. 14. 22:37

2년 전 글에서 sealed class와 enum class에 대해 간략하게 다룬 적이 있다.

 

[Kotlin] sealed class vs. enum class

Kotlin을 처음 배울 때는 enum classsealed class를 혼동하곤 했다. enum을 구현할 떄 enum class 대신 sealed class를 써도 되지 않나? 결론 sealed class를 써도 되지만, 바람직한 사용법은 아니다

thinking-face.tistory.com

위 글에서는 enumerated value를 구현하기 위해 sealed class를 사용할 필요가 없다는 결론을 내렸다. 사실 당연하다. enum class가 멀쩡히 존재하기 때문이다. 그러나 enum class가 있음에도 이런 혼동을 하는 이유는 sealed classenum class가 실제로 매우 비슷하기 때문이다.

 

이 글에서는 sealed class와 enum class가 할 수 있는 일과 할 수 없는 일을 비교해 보고자 한다.

공통점

우선, sealed class와 enum class 모두 상속 관계를 제한한다. sealed class는 해당 클래스가 선언된 파일 안에서만 상속받을 수 있으며, 파일 외부에서는 sealed class를 상속받을 수 없다. 

 

enum class는 애초에 final class이다.

 

또, sealed class와 enum class 모두 함수와 프로퍼티를 가질 수 있으며, 다른 클래스나 인터페이스를 상속받을 수 있다. 조금 특수한 형태이긴 하지만 둘 다 어쨌든 Kotlin 클래스이기 때문.

interface SomeInterface

enum class TestEnum(val order: Int) : SomeInterface {
    VAL1(10),
    VAL2(20),
    VAL3(30);

    fun action() {
        println("Hello! I'm ${this.name}")
    }

    val orderReified: Int
        get() = order * 10
}

sealed class SealedClass(val order: Int) : SomeInterface {
    class SealedSubclass : SealedClass(100)

    val reifiedOrder: Int
        get() = order * 10
}

위 코드처럼 생성자 프로퍼티, getter 프로퍼티, 멤버 함수 등을 모두 선언할 수 있다. 다만 enum class에서는 변수나 함수 등을 enum 값 밑에(VAL1, VAL2, VAL3 밑에) 선언해야 한다. enum class 내부에는 enum 값이 가장 먼저 선언되어야 하기 때문.

차이점

그렇다면 sealed class와 enum class의 차이는 무엇인가?

 

우선, sealed class는 상속 관계를 제한하는 역할을 한다. 즉 sealed class의 하위 타입은 같은 파일에 선언된 서브 클래스로 한정할 수 있다. 그것도 무려 컴파일 시간에!

 

따라서 sealed class를 when 구문 등에서 사용할 때, 코드가 모든 case를 처리하고 있는지 컴파일 시간에 알 수 있으므로 일부 케이스를 빠트리는 등의 실수를 방지할 수 있다.

 

또, sealed class도 어쨌든 class이므로, 인스턴스를 생성할 수 있다. 이때 생성되는 인스턴스는 서로 다르다는 점이 중요하다. 왜냐하면 enum class의 value는 항상 동일한 객체를 반환하기 때문.

 

다음 테스트 코드를 보면 이해될 것이다.

class TestTest {
    @Test
    fun enumTest() {
        val enum1 = TestEnum.VAL1
        val enum2 = TestEnum.VAL1
        println(enum1.hashCode())
        println(enum2.hashCode())
        assert(enum1 === enum2)
    }

    @Test
    fun sealedTest() {
        val sealed1 = SealedClass.SealedSubclass(100)
        val sealed2 = SealedClass.SealedSubclass(100)
        println(sealed1.hashCode())
        println(sealed2.hashCode())
        assert(sealed1 === sealed2)
    }
}

 

enumTest에서는 같은 enum 값의 identity가 동일함을 확인할 수 있다. 

 

그러나 sealedTest에서 생성한 sealed1sealed2는 서로 identical하지 않다. 일단 hashCode부터가 다르고, === 연산으로 비교한 결과도 false이다.

225344427
1604353554

Assertion failed
java.lang.AssertionError: Assertion failed

Sealed class는 동일한 특성을 갖는 객체를 여러 개 만들어낼 수 있는 반면, enum class는 선택지 자체만을 나타낸다고 정리할 수 있겠다. 요컨대 수저는 재료에 따라 금수저, 은수저 등이 존재할 수 있지만(sealed class), 월요일은 월요일 그 자체로 의미를 갖는다는 것(enum class).

 

객체의 의미를 가장 잘 드러낼 수 있는 방법을 고민해 보자.

Real case: navigation route는?

사실 이 글을 작성하게 된 이유는, Compose type-safe navigation에서 사용할 route를 sealed class와 enum class 중 무엇으로 만들어야 할 지 고민하고 있었기 때문이다. Sealed class와 enum class 모두  route를 표현할 수 있기 때문(상속을 제한하므로).

 

이 문제를 해결하려면, navigation argument의 속성을 정확하게 이해하고 있어야 한다.

 

공지 웹뷰 페이지로 가는 route를 NoticeWeb 클래스로 표현한다고 하자. NoticeWeb에는 String 타입의 articleUrl 프로퍼티가 선언되어 있으며, 공지 웹뷰 페이지는 NoticeWeb.articleUrl로 주어진 URL을 로드한다. 즉 서로 다른 articleUrl를 갖는 NoticeWeb 객체가 존재할 수 있다는 뜻이다.

 

따라서 서로 다른 인스턴스를 만들 수 있는 sealed class를 사용하는 것이 적절하다. 매개변수가 없는 route도 통일성을 위해 sealed class로 표현하는 것이 좋다.

애초에 enum class를 못 쓴다

사실 Compose navigation type safe를 정확히 이해했다면 애초에 할 필요가 없는 고민이다. 왜냐면 type-safe route는 클래스를 매개변수로 받기 때문. (정확히는 reified T: Any를 제네릭으로 받는다)

composable<TestSealedClass> {
    // ...
}

composable<TestEnum> {
    // ...
}

composable<TestEnum.VAL1> {
    // 이게 되겠냐
}

그냥 sealed class/data class를 사용하는 게 옳다. 애초에 고민 자체가 잘못된...

 

뭐... 안그래도 sealed class와 enum class를 계속 헷갈리고 있었으니 이번 기회에 잘 정리했다고 생각해야겠다.

'Primary > Kotlin' 카테고리의 다른 글

kapt를 KSP로 migrate하기  (0) 2024.06.08
Kotlin 2.0.0 출시  (0) 2024.06.08
mutableMapOf()의 내부 구현  (0) 2024.02.03
[Kotlin] Coroutines Job  (0) 2022.11.10
[Kotlin] Dispatcher  (0) 2022.11.08
Comments