안드로이드 개발자라면 아마 다음과 코드를 통해 sealed class 에 대해 처음 접했을 것이다.
sealed class UiState {
class Success<out T>(data: T) : UiState()
class Error(e: Throwable?) : UiState()
data object Loading : UiState()
}
봉인된 클래스..? when 절에서 else를 안써도 된다고?
enum class 랑 비슷하네 🤨
잘 모르겠지만 일단 좋은거 같다 그냥 쓰자
대부분의 사람들이 위와 같이 생각했을 것이다.
사실 sealed class 는 abstract class
다! 좀 더 자세하게 말하면, sealed class 는 sub class를 모두 알고 있는 Special 한 abstract class
이다.
이번 포스팅에서는 sealed class 와 abstract class 를 비교해보면서 차이점과 장점에 대해 정리해보고자 한다.
sealed class 가 abstract class 와 다른 점은 sub class 들을 반드시 같은 패키지
에 넣어줘야 한다는 것이다.
컴파일러에게 서브클래스들을 찾는 비용을 줄이기 위해서다.
이러한 제약조건이 없다면 모든 모듈/패키지에 있는 파일에서 sub class가 있는지 Compiler 가 일일이 찾아야 한다. 이러한 오버 헤드를 방지하기 위해 서브 클래스는 반드시같은 패키지
안에 있어야하는 제약 조건을 거는 것이다.
컴파일러는 sealed class의 sub class 들을 모두 알고 있습니다. 따라서,when
절을 사용할 때 else 절을 추가적으로 작성하지 않아도 된다.
sealed class UiState {
class Success<out T>(data: T) : UiState()
class Error(e: Throwable?) : UiState()
data object Loading : UiState()
}
fun foo(uiState: UiState) {
return when (uiState) {
is UiState.Success<*> -> println("Success")
is UiState.Error -> println("Error")
UiState.Loading -> println("Loading")
// else 문을 사용하지 않아도 된다. 😎
}
}
else 를 생략할 수 있기에 얻는 장점이 무엇일까? 🤔
만약, sealed class 가 아닌 abstract class 로 UiState 를 구현했다고 해보자.
abstract class UiState {
class Success<out T>(data: T) : UiState()
class Error(e: Throwable?) : UiState()
data object Loading : UiState()
}
abstract class 의 경우 when 절을 사용할 때 반드시 else 문을 작성해줘야한다. (컴파일러가 sub class들을 모르기 때문)
fun foo(uiState: UiState) = when (uiState) {
is UiState.Success<*> -> println("Success")
is UiState.Error -> println("Error")
UiState.Loading -> println("Loading")
else -> println("else")
}
}
이때, UiState 에 Skeletone 이라는 서브 클래스를 추가한다고 해보자
data object Skeletone: UiState()
Skeletone 이 새롭게 추가 되어도 else
문 때문에, UiState 를 사용하고 있는 곳에서는 아무 에러가 없다.
아무 에러가 없다니.. 좋은거 아닌가??
개발자에게 가장 좋은 Error는 Compile Error 다. Runtime Error 는 Build 할 때 발생하는 에러이기에, 만약 개발자가 이를 처리하지 않고 배포한다면, 사용자가 앱을 사용하다가 어플이 비정상종료될 수 있다.😵
런타임 에러를 방지하기 위해 개발자는 UiState 를 사용하고 있는 클라이언트 코드들을 모두 일일이 찾아 분기처리문을 추가해줘야 한다..🤯 만약, 실수로 한 곳이라도 분기 처리문 처리를 놓친다면 런타임 에러 혹은 논리 에러
가 발생할 것이다.
그럼 이번에는 else 문을 지우고 abstract 키워드를 다시 sealed 키워드로 바꿔주자! 다음과 같이 컴파일 에러가 뜬다.
컴파일러가 발생하기에 개발자는 이를 쉽게 인지할 수 있다.
이제 컴파일 에러가 발생하는 곳에 분기처리문을 추가해주면 된다. 😎
컴파일 에러를 통해 논리 에러를 방지
할 수 있다는 것이 sealed class 의 가장 큰 장점이다!!
따라서,
else
문을 사용한다면 sealed class 를 사용할 이유가 없다고 생각한다.
오케이 sealed class 의 장점에 대해 잘 알았다.
그럼, 이제 무조건 abstract class 가 아닌 sealed class 를 사용하면 되는 걸까?
당연히 아니다. 항상 sealed class를 사용해야 한다면 abstract class 가 삭제됐겠죠? ㅋㅅㅋ
어떤 class 의 구현체를 외부(다른 모듈)에서 구현하도록 설계할 경우가 있다. 이러한 경우, abstract class 를 사용하는 것이 좋다.
보통 Library
가 위의 경우에 해당한다.
Android Library 에서 제공하는 RecyclerView Adapter 는 abstract class 이다. 만약, RecyclerView Adapter 가 sealed class 였다면, 우리는 Adapter를 상속받아 CustomAdapter를 구현하여 사용할 수 없을 것이다..
sealed class의 경우 외부에서 sub class를 만드는 행위가 모두 제한되기 때문이다. 서브 클래스는 반드시 sealed class 가 위치하는 패키지에 존재해야 한다.
// 리사이클러뷰 어뎁터
public abstract static class Adapter<VH extends ViewHolder> {
// 커스텀 어뎁터, 만약 sealed class 라면 상속받아 구현할 수 없다.
class CustomAdapter: RecyclerView.Adapter
정리)
abstract class: 다른 모듈/패키지에서 서브 클래스를 구현해야할 경우
sealed class: 해당 class의 서브 클래스를 해당 패키지에서만 구현할 경우
sealed class UiState {
class Success<out T>(data: T) : UiState()
class Error(e: Throwable?) : UiState()
data object Loading : UiState()
data object Skeleton : UiState()
}
UiState 를 자바로 디컴파일 해보자!
abstract class
로 변환된다.synthetic method
: 컴파일러만 해당 생성자에 접근 가능(다른 클래스에서 생성자 열 수 없음)@Metadata 어노테이션에 있는 정보를 기반으로 Compiler 는 서브 클래스들을 찾는다 ㅎ ㅎ
더 궁금하신 분은 다음 포스팅을 봐주세요 😎
https://medium.com/hongbeomi-dev/sealed-class%EC%99%80-sealed-interface-db1fff634860
https://kotlinlang.org/docs/sealed-classes.html#declare-a-sealed-class-or-interface
안녕하세요 모르던 지식을 배우고 갑니다.
잘 배우고 갑니다. ㅎㅎ