sealed class의 한계와 sealed interface의 등장
sealed class는 클래스를 상속하는 서브 클래스를 컴파일러에서 알 수 있도록 하기 위해 만들어졌다. 아래의 UIState와 같은 간단한 상속구조라면 sealed class로도 충분하다.
sealed class UIState()
data object Loading : UIState()
data class Success(val data: UIData) : UIState()
data class Error(val error: Exception) : UIState()
하지만, sealed class를 사용하면, 다른 클래스들이 둘 이상의 sealed class를 상속받지 못하게 되는 한계가 있다.
예를 들어 다음과 같은 코드를 만들어보자.
sealed class Clickable {
open fun onClick() {}
}
sealed class Draggable {
open fun onDrag() {}
}
class Button : Clickable(), Draggable() {
override fun onClick() {
println("onClick Button")
}
}
그러면 다음과 같은 오류가 생기는 것을 살펴볼 수 있다.
이런 문제가 일어나는 이유는 코틀린의 일반적인 class와 마찬가지로 둘 이상의 sealed class가 하나의 클래스에 상속될 수 없기 때문이다.
물론 다른 한계도 있다. 위 코드에서 볼 수 있듯이 sealed class의 함수를 하위 클래스에서 상속하기 위해서는 open fun으로 선언하고 함수를 구현하거나 함수를 abstract fun으로 만들어야 했다.
sealed class Clickable {
open fun onClick() {
// 빈 구현체
}
}
sealed class Clickable {
abstract fun onClick()
}
sealed interface의 등장
sealed class가 가진 문제를 해결하기 위해 코틀린 1.5 버전에서는 sealed interface 기능이 출시됐다.
sealed interface는 코틀린의 일반적인 interface와 같지만, 컴파일러에 자신을 구현하는 타입을 알려주는 인터페이스이다. 따라서 일반적인 Kotlin interface와 같이 하나의 클래스에서 여러 sealed interface를 구현할 수 있다.
앞에서 sealed class로 선언됐던 Clickable과 Draggable을 sealed interface를 사용하도록 바꾸고 Button이 이를 구현하게 하면 다음과 같아진다.
sealed interface Clickable {
fun onClick()
}
sealed interface Draggable {
fun onDrag()
}
class Button : Clickable, Draggable {
override fun onClick() {
println("onClick Button")
}
override fun onDrag() {
println("onDrag Button")
}
}
Button에 더 이상 오류가 생기지 않는 것을 볼 수 있다.
sealed interface의 사용
sealed interface는 sealed class와 마찬가지로 컴파일러가 서브 타입을 알게 한다. 따라서, when 문에서 어떤 타입의 값들이 왔는지 컴파일 타임에 확인 가능하며, 만약 모든 서브 타입을 when문의 분기문에 넣지 않으면 컴파일 타임에 오류를 일으킨다.
예를 들어 다음과 같은 코드가 있다고 해보자.
class Button : Clickable, Draggable {
override fun onClick() {
println("onClick Button")
}
override fun onDrag() {
println("onDrag Button")
}
}
class Text : Clickable {
override fun onClick() {
println("onClick Text")
}
}
fun main() {
val clickable : Clickable = Button()
when(clickable) {
is Button -> {
clickable.onDrag()
}
}
}
이 코드에서 Text 분기문을 빼고 실행하면 컴파일 타임에 "Text를 분기문(branch)에 더하거나 else 문을 더하라"는 오류를 볼 수 있다.
'when' expression must be exhaustive, add necessary 'is Text' branch or 'else' branch instead
이 문제는 코드를 다음과 같이 바꾸면 해결된다.
fun main() {
val clickable : Clickable = Button()
when(clickable) {
is Button -> {
clickable.onDrag()
}
is Text -> {
clickable.onClick()
}
}
}
정리
정리하면 sealed interface는 sealed class와 같이 컴파일 타임에 컴파일러에서 서브 타입을 알 수 있도록 하는 인터페이스이다.
다른 점은 sealed class는 다중 상속이 불가능한 반면, sealed interface는 다중 구현이 가능하다는 점이다.