Enum vs Sealed Class, Sealed Class로 에러 관리하기

Hyemdooly·2023년 3월 13일
2

Enum Class

Enum classes | Kotlin

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}
  • enum 상수는 객체이다. enum 상수는 쉼표로 구분된다.
  • enum 상수는 해당 메서드와 재정의한 기본 메서드를 사용하여 고유한 익명 클래스를 선언할 수 있다.
enum class ProtocolState {
    WAITING {
        override fun signal() = TALKING
    },

    TALKING {
        override fun signal() = WAITING
    }; // 세미콜론으로 구분

    abstract fun signal(): ProtocolState // Enum이 추상클래스이기 때문에 가능한 일
}
  • enum이 추상클래스이기 때문에 interface 구현이 가능하다.
  • 모든 enum class들은 Comparable interface를 기본적으로 implement한다. → enum class의 상수는 natural order로 정의된다.
  • when 사용 시 모든 케이스를 커버한다면 else가 필요하지 않다.
enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

fun main() {
    val direction = Direction.NORTH
    when(direction) {
        Direction.NORTH -> println("북쪽!")
        Direction.SOUTH -> println("남쪽!")
        Direction.WEST -> println("동쪽!")
        Direction.EAST -> println("서쪽!")
    }
}

Sealed Class

Sealed classes | Kotlin

sealed interface Error

sealed class IOError(): Error

class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()

object RuntimeError : Error // 상태값이 바뀌지 않는 경우 사용
  • sealed class의 모든 subclass는 compile time에 알 수 있다. (컴파일러에서 sealed class의 자식 클래스가 어떤 것이 있는지 알 수 있음)
  • 추상 클래스이다. 바로 인스턴스화할 수 없고 abstracts member을 가질 수 있다.
  • subclass는 같은 패키지 내에서만 선언할 수 있다
  • 생성자는 protected(기본) 또는 private
  • sealed class를 사용할 때 핵심은 when을 사용할 때!
    모든 케이스를 커버했다는 것을 확실히 알 수 있어 else가 필요하지 않다. → 첫번째 이유 때문
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}") }
    is RuntimeError ->  { println("Runtime error") }
    // the `else` clause is not required because all the cases are covered
}

Enum vs Sealed Class

공통점

  • 추상 클래스이다.
  • enum type을 위한 value 집합이 제한된다. (하위 클래스의 타입이 정해져 있음)
  • when 사용 시 else가 필요 없다.

차이점

  • 상속이 불가능하기 때문에 enum class의 서브 클래스를 생성할 수 없지만, sealed class는 가능하다.
  • enum class에서는 각 enum 상수가 single instance로만 존재한다. (싱글톤) → 상수 정의 이후에 상태값을 변경할 수 없다.
  • 하지만 sealed class의 subclass는 각각 고유한 상태를 가진 multiple instance로 존재할 수 있다. → 상태값을 유동적으로 변경할 수 있다.

Sealed Class와 Generic : 에러 관리하기

fun readName(): List<String> {
    val input = readln().replace(" ", "").split(",")
    input.forEach {
        verifyName(it) ?: return readName()
    }
    return input
}

private fun verifyName(name: String): String? {
    return when {
        name.isBlank() -> {
            println("입력 값이 비었습니다")
            null
        }

        !Regex("[가-힣a-zA-Z]+").matches(name) -> {
            println("입력 값이 문자가 아닙니다")
            null
        }

        else -> {
            name
        }
    }
}

코드를 작성하면서 사용자 입력 에러를 관리해야할 일이 생겼다. null을 받으면 입력을 반복하게끔 해야하는데, depth가 깊어지고 라인 수도 길어져서 마음에 안드는 상황이었다. 이 때, sealed class를 활용하는 방법을 찾았다.

sealed class InputState<out T> { // generic 사용
    class Success<out T>(val input: T) : InputState<T>()
    class Error(val error: String) : InputState<Nothing>()
}

위와 같이 InputState를 정의해주었다. 예시로 제시한 코드에서는 String의 타입만 사용하기 때문에 사실 Generic을 사용하지 않아도 괜찮지만, 전체 코드에서는 String뿐만 아니라 다양한 타입을 입력받고 검증하고 있기 때문에 Generic을 적용해주었다.

fun readName(): List<String> {
    val input = readln().replace(" ", "").split(",")
    input.forEach {
        verifyName(it) ?: return readName()
    }
    return input
}
private fun verifyName(name: String): String? {
    val isError = when {
        name.isBlank() -> InputState.Error("입력 값이 비었습니다")
        !Regex("[가-힣a-zA-Z]+").matches(name) -> InputState.Error("입력 값이 문자가 아닙니다")
        else -> InputState.Success(name) // InputState<String> 타입
    }
    if (isError is InputState.Error) {
        println(isError.error)
        return null
    }
    return name
}

다양한 에러 상황에 대해 알맞는 출력 문구를 넣어줄 수 있었다. 검증할 때 Error였다면 문구를 출력하고 null을 리턴하는 방식이다.

이렇게 sealed class는 상수 값을 갖는 enum과 달리 변경 가능한 값을 가지면서, 오직 객체 하나만을 가지는 enum과 달리 객체를 여러개 생성할 수 있으면서 enum과 비슷한 일을 해줄 수 있다. 값을 유동적으로 변동할 수 있다는 것이 핵심이다!

0개의 댓글