관리 메뉴

나만을 위한 블로그

[Kotlin] Sealed Class와 Sealed Interface란? 본문

개인 공부/Kotlin

[Kotlin] Sealed Class와 Sealed Interface란?

참깨빵위에참깨빵 2023. 6. 6. 23:36
728x90
반응형

sealed class에 대해선 이전에 data class와 같이 포스팅을 작성한 적이 있다.

 

https://onlyfor-me-blog.tistory.com/454

 

[Kotlin] 코틀린에서 제공하는 특수 클래스(Data Class, Sealed Class)

코틀린에는 자바와 달리 특수한 클래스가 존재한다. Data Class와 Sealed Class라는 것인데 각각 어떤 것인지 정리한다. Data Class 코틀린 공식 홈페이지에서 설명하는 Data Class는 아래와 같다. https://kotli

onlyfor-me-blog.tistory.com

 

그러나 sealed interface를 사용하게 되면서 sealed 키워드가 겹치는 두 요소에 대해 포스팅을 작성하는 것도 재밌겠다 싶어서 작성한다.

sealed class와 sealed interface를 설명하는 공식 문서부터 확인한다.

 

https://kotlinlang.org/docs/sealed-classes.html

 

Sealed classes and interfaces | Kotlin

 

kotlinlang.org

sealed class와 sealed interface는 상속에 대한 더 많은 제어를 제공하는 제한된 클래스(restricted class) 계층을 나타낸다. sealed class의 모든 직접적인 하위 클래스는 컴파일 타임에 알려진다. sealed class가 정의된 모듈 및 패키지 외부에선 다른 하위 클래스가 나타날 수 없다. 예를 들어 타사 클라이언트(third-party clients)는 코드에서 sealed class를 상속할 수 없다. 따라서 sealed class의 각 인스턴스에는 이 클래스가 컴파일 될 때 알려진 제한된 세트의 유형이 있다. sealed interface에 대해서도 동일하게 작동한다. sealed interface가 있는 모듈이 컴파일되면 새 구현이 나타날 수 없다
어떤 의미에서 sealed class는 enum class와 유사하다. enum에 대한 값 집합도 제한되지만 각 enum 상수는 하나의 인스턴스로만 존재하는 반면, sealed class의 하위 클래스는 각각의 상태를 가진 여러 인스턴스를 가질 수 있다

예를 들어 라이브러리의 API의 경우, 라이브러리 사용자가 발생시킬 수 있는 오류를 처리할 수 있도록 오류 클래스를 포함할 가능성이 높다. 이런 오류 클래스의 계층 구조에 공개 API에서 볼 수 있는 인터페이스 또는 추상 클래스가 포함돼 있으면 클라이언트 코드에서 구현하거나 확장하는 걸 막을 수 없다. 그러나 라이브러리는 외부에서 선언된 오류를 알지 못하므로, 자체 클래스로 일관되게 처리할 수 없다. 오류 클래스의 봉인된 계층 구조를 통해 라이브러리 작성자는 가능한 모든 오류 유형을 알고 있으며 나중에 다른 유형이 나타나지 않게 할 수 있다...(중략)...sealed class는 그 자체로 추상 클래스고 직접 인스턴스화할 수 없으며, 추상 멤버를 가질 수 없다. sealed class의 생성자는 protected(기본값) 또는 private의 두 가시성 중 하나를 가질 수 있다

sealed class, sealed interface의 직접적인 하위 클래스는 같은 패키지에서 선언돼야 한다. 이것들은 최상위 수준이거나 다른 명명된(named) 클래스, 명명된 인터페이스 또는 명명된 객체의 수에 관계없이 중첩될 수 있다. 하위 클래스는 코틀린의 일반 상속 규칙과 호환되는 한 모든 가시성을 가질 수 있다. sealed class의 하위 클래스에는 적절한 정규화된 이름이 있어야 한다...(중략)...sealed class를 쓰는 주요 이점은 when expression에서 발휘된다. expression이 모든 경우를 포함하는지 확인할 수 있는 경우, expression에 else 절을 추가할 필요가 없다
fun log(e: Error) = when(e) {
    is FileReadError -> { println("Error while reading file ${e.file}") }
    is DatabaseError -> { println("Error while reading from database ${e.source}") }
    is RuntimeError ->  { println("Runtime error") }
    // the `else` clause is not required because all the cases are covered
    // 모든 경우에 적용되므로 else 절이 필요없다
}

 

공식문서의 핵심을 정리하면 아래와 같을 것이다.

 

  • sealed class, sealed interface는 자신을 상속할 때 제한된 조건에서만 가능하도록 하는 클래스다
  • 둘은 컴파일 타임에 구체화된다
  • sealed class, sealed interface의 하위 클래스는 각각 다른 상태를 가진 인스턴스를 여럿 만들 수 있다
  • sealed class, sealed interface의 접근 제어자는 protected가 기본값이고, private 중 하나만 선택 가능하다

 

공식문서 외에선 어떻게 설명하는지 확인한다.

 

https://tourspace.tistory.com/467

 

[Kotlin] Enum의 대체 - Sealed class / Sealed interface 정리

Sealed class / interface의 개념은 kotlin 초반 버전부터 진작에 나왔습니다. 따라서 이에 대한 활용법도 이미 많이 나와있는 상태인데, 대부분이 network response를 다루는 예제를 이용하여 설명하고 있습

tourspace.tistory.com

(중략)...enum만으로도 타입을 제한하고 when 문을 사용할 경우 누락된 값의 처리를 IDE를 통해 알 수 있지만, 이는 내부적으로 각 value당 하나의 인스턴스를 사용하기 때문에 서로 다른 형태를 가질 수 있다. SUCCESS인 경우 성공에 대한 결과값, FAIL인 경우 exception에 대한 정보를 넣고 싶어도, 정보가 서로 다른 타입이기 때문에 꼭 구현해야겠다면 추상 클래스를 사용해야 한다. 하지만 추상 클래스를 어떤 클래스가 상속받을지 컴파일러가 알 수 없기 때문에 확장성은 생겼지만 enum처럼 타입 제한을 할 수 없다
enum과 같은 value 당 제한을 가지면서 각 value의 형태를 다르게, 확장성 있도록 가져가기 위해 sealed class를 사용한다...(중략)

 

https://blog.logrocket.com/guide-using-sealed-classes-kotlin/

 

Guide to using sealed classes in Kotlin - LogRocket Blog

Kotlin's sealed classes can be more performant and flexible than enum or abstract classes. Learn how to use them in this tutorial.

blog.logrocket.com

sealed class는 제한된 클래스 계층 구조를 나타낸다. 이를 통해 부모 함수 범위 안에서 하위 클래스를 정의해 계층 구조를 나타낼 수 있다. 이 경우 자식 또는 하위 클래스는 데이터 클래스, 객체, 일반 클래스 또는 sealed class 등 모든 유형이 될 수 있다. sealed class는 자신을 구현할 수 있는 다른 클래스를 제한할 수 있다. 이렇게 하면 프로젝트에서 제한된 값을 가짐으로써 응용 프로그램 안에서 제한된 가능성 집합을 나타낼 수 있다. sealed class는 enum class의 확장이다. enum class는 각 값의 단일 인스턴스만 허용한다. 우리는 일반적으로 enum을 써서 같은 유형의 상수값을 저장한다. enum class도 제한된 계층이다. 그러나 각 enum 상수는 단일 인스턴스로만 존재하지만 sealed class의 하위 클래스는 여러 인스턴스를 가질 수 있다. enum 및 sealed class는 일반적으로 가능성 집합의 여러 값이 있는 형식을 나타내는 데 쓰인다

 

하지만 중요한 건 실제로 어떻게 쓰냐는 거다. 예전에 Coroutine Flow를 써서 MVVM 아키텍처를 적용한 예제 포스팅을 작성한 적이 있는데 그 코드 중 일부를 확인해보자.

 

sealed class ApiState<T>(
    val data: T? = null,
    val message: String? = null
) {
    class Success<T>(data: T) : ApiState<T>(data)
    class Error<T>(message: String, data: T? = null) : ApiState<T>(data, message)
    class Loading<T> : ApiState<T>()
}

 

이 sealed class는 API 응답 상태를 나타내기 위해 만들어진 클래스다. 클래스명 뒤에 <T>를 씀으로써 다양한 형태의 data class를 쓸 수 있도록 했다. 이 sealed class 안에는 ApiState를 상속하는 3개 클래스가 있다.

 

  • Success : API 호출에 성공한 경우 API에서 가져온 data를 담는 클래스다. 필요한 곳에서 data 참조변수를 통해 API로부터 가져온 데이터에 접근할 수 있다
  • Error : API 호출에 실패한 경우 에러 메시지와 nullable한 API data를 가져온다. 에러 발생 시 API가 리턴하는 값은 저마다 달라서, 어떤 데이터도 주지 않거나 에러 메시지 등 일부 응답만 줄 수도 있다. 이것에 대처하기 위해 제네릭 타입 T 뒤에 ?를 써서 nullable하게 만들었다. 이것이 null인 경우를 따져 엘비스 연산자를 쓰거나 해서 보다 스무스하게 에러 처리를 할 수 있을 것이다
  • Loading : API 호출이 진행 중일 때 사용하는 클래스다. 호출 중이기 때문에 ApiState 클래스 생성자로 아무것도 넘기지 않으며, Loading 상태일 때 프로그레스 바나 로띠 이미지 등을 표시하게 할 수 있다

 

그리고 여차저차해서 액티비티에서 아래와 같이 사용할 수 있다.

 

githubRepositories.collect {
    when (it) {
        is ApiState.Success -> {
            it.data?.let { data ->
                Log.e(TAG, "data - incomplete_results : ${data.incompleteResults}")
                val list = data.items
                for (i in list.indices) {
                    Log.e(TAG, "license : ${list[i].license}")
                }
            }
            mGithubRepositories.value = ApiState.Loading()
        }
        is ApiState.Error -> {
            Log.e(TAG, "## 에러 : ${it.message}")
            mGithubRepositories.value = ApiState.Loading()
        }
        is ApiState.Loading -> {}
    }
}

 

githubRepositories는 뷰모델의 StateFlow다. 액티비티에서 이 변수를 가져와 collect {}를 사용해 데이터들을 가져오는데, 이 때 리시버 it의 타입을 IDE에서 확인하면 ApiState<data class 이름>인 것을 볼 수 있다. 그래서 어떤 클래스냐에 따라 when을 써서 각각 다른 처리를 수행한다. 실제로 else 절이 없어도 컴파일 에러가 발생하지 않는다. 예제 코드기 때문에 참고용으로만 확인하자.

 

이처럼 sealed class, sealed interface는 API 응답 상태를 나타낼 때 사용할 수 있다. 이 말은 Navigation Component를 사용할 경우 화면 이동 이벤트 처리, UI 상태 관리에서도 활용할 수 있다는 뜻이다.

반응형
Comments