[Kotlin] Sealed Class 알아보기

Jae Eon·2022년 1월 2일
0

Kotlin 공부

목록 보기
6/6

회사에서 Java 17버전을 사용하게 될 이유중 하나가 Sealed Class의 개선이라는 이야기를 듣고
Sealed Class가 어떤역할을 하는지 궁금하여 정리한 포스트.

🍎 Sealed Class란?

우선 공식문서의 링크를 걸고 설명하겠다. ➡️ 공식문서 링크

Sealed 클래스의 하위 클래스는 컴파일시 정보가 등록됩니다.

이말은 컴파일 후 동적으로 하위 클래스및 구현체가 추가(구현) 될 수 없음을 의미합니다.

따라서, Sealed 클래스는 Super 클래스를 상속받는 Child 클래스의 종류를 제한 할 수 있습니다.

문서를 간단히 정리 하자면

  • Sealed Class 내에 정의된 Class 이외의 형태는 가질 수 없는, 제한된 계층구조를 정의하는 Class
  • Enum class 내의 서브 클래스가 동일 클래스만을 사용할 수 있을 때, Sealed 클래스는 클래스 종류에 제약을 받지 않는다.
  • Sealed class는 추상 클래스로써 자기 자신을 사용해 생성할 수 없으며, 내부에 선언한 클래스들을 통해 생성해야 한다.
  • Sealed class는 public 생성자를 가질 수 없다(기본적으로 private 형태를 띔)
  • 프로젝트 내 어디에서나 바로 사용이 가능하다.

🍑 Sealed Class Vs Enum Class

Enum Class와 유사한 특징으로 여러 타입의 집합이라는 점이 있지만, 차이점이 존재합니다.

  • Sealed 클래스는 각각의 고유한 여러 인스턴스를 가질 수 있고 Enum 클래스는 단일 인스턴스로만 이루어져 있습니다.

  • Enum class 내의 서브 클래스가 동일 클래스만을 사용할 수 있지만, Sealed 클래스는 클래스 종류에 제약을 받지 않습니다(여러 클래스 사용가능).

동일 클래스와 여러클래스? 이해하기 어려울 수 있어 아래 정의하기 목차에서 코드로 설명하겠습니다. 👍

🍊 Sealed Class 정의하기

// 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를 이용한다.

🍋 Sealed Class 사용시 이점

위에서 구현한 Error의 경우 사용자가 Error 클래스들을 각각 핸들링 후 Enum의 description을 출력 해주면 될 것 같은데
왜 굳이 sealed 클래스를 사용하는지 의문이 들 수도있다.

Sealed 클래스를 사용하면 다음과 같은 이점을 가진다.

  • 특정 Class들이 어떤 클래스들에 대한 집합인지 명확히 확인할 수 있다.
  • when 구문을 사용할 때, 사용하지 않은 inner class에 대해 정의할 필요가 없어 enum에 비해 유연하다.
  • 동일한 행위를 필요로 하는 요구사항 안에서, 새로운 function의 추가 없이 기존 function에 추가 혹은 제거가 용이하다.
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가 없다면 컴파일 에러가 발생해 사용자 실수를 줄여 줄 수있다.

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하나로 간소화 된 것을 볼 수 있다.
코드 참조(링크)

또한 Generic 타입을 사용하여 한단계 더 타입 추상화 할 수있는데 이또한 코드로 설명하겠다.

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")
        }
    }
}

위처럼 제네릭 타입을 사용해서 타입 추상화 까지 시킬 수있다.

profile
🖋정리를 안하면 잊어버린다.👣한 발자국씩 가보자!

0개의 댓글