Sealed class 정의
sealed calss는 Super 클래스를 상속 받는 Child 클래스의 종류를 제한하는 특성을 갖는 클래스이다.
어떤 Super 클래스를 상속받는 하위 클래스는 여러 파일에 존재하기 때문에 컴파일러는 해당 Super 클래스를 상속받는 하위 클래스가 얼마나 있는지 알지 못한다.
하지만 sealed class 는 동일한 패키지에 정의된 자신의 하위 클래스들을 인지해 컴파일러에서 그 외 다른 하위클래스는 존재하지 않음을 알 수 있다. 아래 예시를 통해 해당 상황을 보도록 하겠다.
abstract class DogState
class Running : DogState()
class Eating : DogState()
class Barking : DogState()
강아지의 상태를 클래스로 만든다고 가정해보자. 뛰는 상황, 먹는 상황, 짖는 상황 이렇게 3가지의 상태를 가질 수 있다.
각 강아지의 상태별로 상태 메시지를 얻기 위해 상태 메시지를 얻는 함수를 작성하면 [그림 1]과 같이 작성할 수 있다.
하지만 [그림 1] 처럼 코드를 작성하면 when 문에서 오류가 발생한다.
오류를 자세히 보면 'when' expression must be exhaustive, add necessary 'else' branch 즉 else branch를 추가해야 한다는 의미이다.
그냥 추상 클래스로 상속을 받아 만들었을 때, 왜 else branch가 필요할까?
그 이유는 컴파일러가 DogState를 상속받는 하위클래스들을 알지 못하기 때문이다.
그러면 그냥 barking 을 else로 두고 처리하면되는 것 아닌가? 라는 의문이 생길 수 있다.
그렇게 되면 running 와 eating 상태가 아닌 barking을 포함한 다른 상태들도 해당 조건에 걸리기 때문에 barking 에 대한 처리가 안될 뿐더러 해당 문제는 컴파일 단에서 오류로 찍히지 않기 때문에 문제를 파악하기 어려워진다.
따라서 이런 문제를 sealed class를 통해서 해결할 수 있다.
Sealed class 사용 예제
앞선 정의와 마찬가지로 sealed class는 child class의 종류를 제한하기 때문에, 컴파일러에서 sealed class의 자식클래스가 어떤 것이 있는지 파악할 수 있다.
예시로 본 추상 클래스를 sealed class로 변경해서 다시 보도록 하겠다.
sealed class DogState
class Running : DogState()
class Eating : DogState()
class Barking : DogState()
[그림 2] 사진을 보았듯이 [그림 1]과 달리 else 문 처리를 하지 않아도 오류가 뜨지 않는다.
왜냐하면 컴파일러에서 sealed class인 DogState의 자식 클래스가 Running, Eating, Barking 이 세가지만 존재함을 파악하고 있기 때문이다.
만약 DogState를 상속받는 Walking 이라는 child 클래스를 하나 더 만들고 when문에 처리하지 않았다고 가정해보자.
그러면 when 문에서 sealed class를 상속받는 Walking이라는 클래스에 대한 처리가 없다는 오류가 발생한다.
즉 컴파일러에서 DogState를 상속받는 하위 클래스의 종류를 알고있기 때문에 가능한 일이다.
Sealed class 특징
- sealed class는 앞에 sealed 라는 키워드를 붙여 정의한다.
- sealed class는 abstract 클래스로, 객체를 생성할 수 없다.
- sealed class 의 생성자는 private 이다. public으로 설정할 수 없다.
- sealed class와 하위클래스는 동일한 파일(같은 패키지) 에 정의되어야 한다.
다른 패키지에 정의될 경우 같은 패키지에 있는 자식 클래스만 상속이 가능하다는 오류가 발생한다.
- sealed class의 하위 클래스는 class, data class, object class 로 정의할 수 있다.
Sealed class를 Generics으로 정의
앞선 예제에서는 sealed 클래스 내부에서 사용할 데이터 타입을 지정해서 작성해보았다면 외부에서 지정하는 제네릭으로 sealed class를 활용해보겠다.
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val exception: Exception) : Result()
object Loading: Result()
}
어떤 요청에 대한 결과값의 상태를 갖고 있는 Result 클래스를 생성한 코드이다.
애플리케이션이 서버에서 어떤 데이터를 요청한다고 가정할 때 성공하면 Success, 실패하면 Error, 아직 처리가 진행 중이면 Loading 을 리턴한다고 가정해보자.
아래와 같이 각 상태에 대해 객체들이 리턴될 수 있다.
var parsedResult : Result =
Result.Success("Sealed classes are used for representing...")
showResult(parsedResult)
parsedResult =
Result.Error(Exception("Got error while parsing this url"))
showResult(parsedResult)
parsedResult = Result.Loading
showResult(parsedResult)
fun showResult(result: Result) {
when (result) {
is Result.Success -> {
println("Success: ${result.data}")
}
is Result.Error -> {
println("Error: ${result.exception}")
}
is Result.Loading -> {
println("In progress")
}
}
}
Success: Sealed classes are used for representing...
Error: java.lang.Exception: Got error while parsing this url
In progress
예시 코드를 실행시키면 위와 같이 메시지들이 출력되는걸 볼 수 있다.
위 코드를 제네릭으로 변경한다면 (제네릭이란 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다.) 아래와 같이 바꿀 수 있다. 더 자세한 제네릭 관련 글은 아래 글을 참고하길 바란다.
https://codechacha.com/ko/generics-class-function-in-kotlin/
sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
data class Error<out T : Any>(val exception: Exception) : Result<T>()
object Loading: Result<Nothing>()
}
클래스 이름 옆에 <out T : Any> 를 작성해주면 된다. 이는 T에 어떤 데이터 타입이든 올 수 있음을 의미한다.
Success와 Error는 어떤 데이터 타입인 T가 사용되므로 Result<T>() 를 리턴하고,
Loading은 어떤 데이터 타입 T를 사용하지 않기 때문에 Result<Noting>을 리턴한다.
이렇게 만들어진 제네릭 sealed class는 아래와 같이 사용될 수 있다.
var parsedResult : Result<String> =
Result.Success("Sealed classes are used for representing...")
showResult1(parsedResult)
parsedResult =
Result.Error(Exception("Got error while parsing this url"))
showResult1(parsedResult)
parsedResult = Result.Loading
showResult1(parsedResult)
fun showResult1(result: Result<String>) {
when (result) {
is Result.Success -> {
println("Success: ${result.data}")
}
is Result.Error -> {
println("Error: ${result.exception}")
}
is Result.Loading -> {
println("In progress")
}
}
}
마치며
UI State를 관리하는 과정에서 sealed class가 사용되는 것을보고 공부하게 되었는데,
이 포스팅을 작성하면서 sealed class가 무엇이고, 이를 활용하는 방법들을 알 수 있게 되었다.
참고 레퍼런스
'Language > Kotlin' 카테고리의 다른 글
[Kotlin] isNullOrBlank vs isNullOrEmpty 헷갈리지말자 (0) | 2023.08.30 |
---|---|
[Kotlin 문법] 스코프함수 (0) | 2023.01.06 |
[Kotlin 문법] 오버라이딩과 추상화 (0) | 2023.01.06 |
[Kotlin] 클래스와 상속 (0) | 2023.01.06 |