회사에서 Java 17버전을 사용하게 될 이유중 하나가 Sealed Class의 개선이라는 이야기를 듣고
Sealed Class가 어떤역할을 하는지 궁금하여 정리한 포스트.
우선 공식문서의 링크를 걸고 설명하겠다. ➡️ 공식문서 링크
Sealed 클래스의 하위 클래스는 컴파일시 정보가 등록됩니다.
이말은 컴파일 후 동적으로 하위 클래스및 구현체가 추가(구현) 될 수 없음을 의미합니다.
따라서, Sealed 클래스는 Super 클래스를 상속받는 Child 클래스의 종류를 제한 할 수 있습니다.
문서를 간단히 정리 하자면
Enum Class와 유사한 특징으로 여러 타입의 집합이라는 점이 있지만, 차이점이 존재합니다.
Sealed 클래스는 각각의 고유한 여러 인스턴스를 가질 수 있고 Enum 클래스는 단일 인스턴스로만 이루어져 있습니다.
Enum class 내의 서브 클래스가 동일 클래스만을 사용할 수 있지만, Sealed 클래스는 클래스 종류에 제약을 받지 않습니다(여러 클래스 사용가능).
동일 클래스와 여러클래스? 이해하기 어려울 수 있어 아래 정의하기 목차에서 코드로 설명하겠습니다. 👍
// enum 생성
enum class IOError(description: String) {
FILE_READ_ERROR("파일 읽기 실패"), DATA_BASE_ERROR("데이터 베이스 접근 실패")
}
위 Enum 클래스 생성을 살펴보면 String
type의 description이 생성자로 있는 클래스들의 집합임을 알 수있다.
enum의 내 서브 클래스의 형식이 고정되어 버린 것이다.
// sealed 생성
sealed interface Error
sealed class IOError(): Error
class FileReadError(val f: File): IOError()
class DatabaseError(val source: DataSource): IOError()
// class JustError(): IOError() <--사용시 Warning 발생
object RuntimeError : Error
Sealed 클래스 생성은 위와같이 File
type이나 DataSource
이 포함된 클래스도 선언을 할 수있다.
이 코드에서 object와 class 타입으로 각각 선언했을때의 차이점이 있다.
Warnig 발생이라고 적어둔 부분에서 'sealed' subclass has no state and no overridden 'equals()'
라는 경고가 나타나는데,
상태(변수)가 있거나 equals를 override할 경우에만 class로 상속 받으라는 이야기이다. 그 이외에는 메모리 절약을 위해 object를 이용한다.
위에서 구현한 Error의 경우 사용자가 Error 클래스들을 각각 핸들링 후 Enum의 description을 출력 해주면 될 것 같은데
왜 굳이 sealed 클래스를 사용하는지 의문이 들 수도있다.
Sealed 클래스를 사용하면 다음과 같은 이점을 가진다.
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}") }
RuntimeError -> { println("Runtime error") }
// the `else` clause is not required because all the cases are covered
}
위 코드는 when을 사용할때 else 없이 사용을 할 수 있고 else가 없다면 컴파일 에러가 발생해 사용자 실수를 줄여 줄 수있다.
// 1. 각기 다르게 선언된 별개의 data class
data class Wheel(val wheelSize: Int, val manufacturer: String)
data class Gear(val gearCount: Int, val isManual: Boolean)
data class Name(val carName: String, val manufacturer: String)
// 2. 개별 클래스들을 처리하고 이를 업데이트 하는 function
override fun processWheel() {
model.loadWheel().let { wheel->
view.updateWheel(Wheel(wheel.size, wheel.manufacturer))
}
}
override fun processGear() {
model.loadGear().let { gear->
view.updateGear(Gear(gear.count, gear.isManual))
}
}
override fun processName() {
model.loadName().let { name->
view.updateName(Name(name.carName, name.manufacturer))
}
}
// 3. 3가지의 update객체 코드가 생성됨
override fun updateWheel(wheel: Wheel) {
binding.wheelSize.text = wheel.wheelSize.toString()
binding.manufacturer.text = wheel.manufacturer
}
override fun updateGear(gear: Gear) {
binding.gearCount.text = gear.gearCount.toString()
binding.isManual.text = gear.isManual.toString()
}
override fun updateName(name: Name) {
binding.carName.text = name.carName
binding.manufacturer.text = name.manufacturer
}
위 코드를 보면 객체마다 fun을 정의해서 각각 업데이트객체
를 만드는 것을 알 수있다.
view.updateGear
, view.updateWheel
, view.updateName
3개가 생성 된다.
이를 Seald Class를 사용하여 개선을 해보면
// 1. sealed class를 사용해 묶음
sealed class CarComponent {
data class Wheel(val wheelSize: Int, val manufacturer: String): CarComponent()
data class Gear(val gearCount: Int, val isManual: Boolean): CarComponent()
data class Name(val carName: String, val manufacturer: String): CarComponent()
}
// 2. 개별 클래스들을 처리하고 이를 업데이트 하는 function
override fun processWheel() {
model.loadWheel().let { wheel->
view.updateCarComponent(CarComponent.Wheel(wheel.size, wheel.manufacturer))
}
}
override fun processGear() {
model.loadGear().let { gear->
view.updateCarComponent(CarComponent.Gear(gear.count, gear.isManual))
}
}
override fun processName() {
model.loadName().let { name->
view.updateCarComponent(CarComponent.Name(name.carName, name.manufacturer))
}
}
// when 조건식을 사용하여 코드 간소화
override fun updateCarComponent(component : CarComponent) {
when (component) {
is CarComponent.Wheel -> {
binding.wheelSize.text = wheel.wheelSize.toString()
binding.manufacturer.text = wheel.manufacturer
}
is CarComponent.Gear -> {
binding.gearCount.text = gear.gearCount.toString()
binding.isManual.text = gear.isManual.toString()
}
is CarComponent.Name -> {
binding.carName.text = name.carName
binding.manufacturer.text = name.manufacturer
}
}
}
위처럼 view.updateCarComponent
하나로 간소화 된 것을 볼 수 있다.
코드 참조(링크)
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 InProgress: Result<Nothing>()
}
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.InProgress
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.InProgress -> {
println("In progress")
}
}
}
위처럼 제네릭 타입을 사용해서 타입 추상화 까지 시킬 수있다.