Kotlin

enum class, sealed class

kakaroo 2022. 3. 31. 22:52
반응형

 

enum class

 

enum class는 enum 예약어로 만들며 클래스 내에 문자열 상수를 열거해 선언합니다.

열거한 상수는 사실 객체이며, 기본으로 name과 ordinal 프로퍼티를 제공합니다.

name은 열거 상수의 문자열이며, ordinal은 열거한 순서를 나타내는 인덱스 번호입니다.

 

열거형 클래스에는 기본으로 열거한 상수 모두를 객체로 가져오는 values() 함수와 인수로 전달한 문자열에 해당하는 열거 상수를 가져오는 valueOf() 함수를 제공합니다.

 

enum class Direction(val no: Int) {
    NORTH(10), EAST(20), WEST(30), SOUTH(40)	//Direction을 상속받는 클래스이므로 상위 클래스의 생성자에 맞추어 호출
}
fun main(args: Array<String>) {
    println(Direction.values()[0])  //NORTH
    println(Direction.valueOf(Direction.EAST.name))   //EAST
    val direction: Direction = Direction.WEST
    println(direction.ordinal)      //2 : Index
}

 

열거 상수는 enum 예약어로 선언한 클래스를 상속받는 클래스의 객체입니다. 이 때 서브 클래스의 이름은 없으며 이러한 클래스를 익명 클래스 (Anonymous class)라고 합니다.

예를 들어, enum class Direction { NORTH } 라고 만들면 NORTH 는 Direction 이라는 클래스를 상속받는 클래스의 객체입니다. 따라서 enum class Direction(val no: Int) 라고 열거형 클래스를 선언하면 하위 클래스에서는 상위 클래스의 생성자에 맞추어 호출해야 하므로 NORTH(0) 이라고 표현한 것입니다.

 

열거 상수가 익명 클래스의 객체이므로 익명 클래스를 직접 정의해서 이용할 수 있습니다.

enum class Direction {
    NORTH {
        override val data: Int = 10
        override fun func() { println("North") }
        },  //상수는 콤마(,)로 구분
    EAST {
        override val data: Int = 20
        override fun func() { println("EAST") }
         },
    WEST {
        override val data: Int = 30
        override fun func() { println("WEST") }
    },
    SOUTH {
        override val data: Int = 40
        override fun func() { println("SOUTH") }
    };  //상수와 프로퍼티 or 함수를 세미콜론(;)으로 구분

    abstract val data: Int
    abstract fun func()
}
 

enum class는 코드가 단순해지고, 가독성이 좋아 주로 사용합니다.

인스턴스 생성과 상속을 방지하며, 상수값의 타입 안정성이 보장됩니다.

 

전송 타입에 따른 신호를 정의한 enum class가 아래와 같이 있습니다.

(주기적인 값을 가지는 PERIODIC SIGNAL과 주기적인 값이 없는 DIRECT SIGNAL이 있다고 가정.)

 

인덱스로 값을 가져 올 수 있음

enum class SearchEngine(val value: Int, val valueName: String, val url: String) {
    ENGINE_NAVER(0, "Naver", PAGE_URL_NAVER),
    ENGINE_GOOGLE(1, "Google", PAGE_URL_GOOGLE)
}

private fun getSearchEngine(engineType: Int) = Common.SearchEngine.values()[engineType]

var url = getSearchEngine(engineType).url

 

enum class는 아래와 같은 제약사항이 있습니다.

 

단, 하나의 인스턴스만 존재하게 되므로 최초에 설정한 enum 각각에 대한 상태를 변경할 수가 없습니다.

주 생성자 매개변수에 String과 Int 타입의 프로퍼티를 선언했다면 모든 열거 상수는 String과 Int 타입으로 구성해야 합니다. 즉, 데이터는 추가할 수는 있는데 열거 객체별로 데이터 구성을 다르게 할 수는 없습니다.

그리고, 상속이 불가능하기 때문에 sub class를 생성할 수도 없습니다.

코틀린에서는 이러한 제약을 sealed class 로 해결할 수 있습니다.

 

 

sealed class

자기 자신이 추상 클래스이고, 자신을 상속받는 여러 서브 클래스들을 가질 수 있습니다. 이를 사용하면 enum 클래스와 달리 상속을 지원하기 때문에, 상속을 활용한 풍부한 동작을 구현할 수 있습니다.

그리고 자신을 상속받는 서브 클래스의 종류를 제한할 수 있습니다. 왜냐하면 sealed 클래스는 다음과 같은 특성을 지니기 때문입니다.

 

  • sealed 클래스의 서브 클래스들은 반드시 같은 파일 내에 선언되어야 함
  • 단, sealed 클래스의 서브 클래스를 상속한 클래스들은 같은 파일 내에 없어도 됨
  • sealed 클래스는 기본적으로 abstract 클래스임 ==> 객체 생성이 불가능함
  • sealed 클래스는 private 생성자만 갖게 됨

 

서브 클래스 각각은 각기 다른 값을 가지고 개성있게 생성될 수 있도록 되어 있습니다. 서브 클래스들은 class, data class, object 모두 가능합니다.

다시 한 번 말하지만, 이러한 서브 클래스들은 분명 같은 파일 안에 선언해야 하고 같은 파일 내에 있다면 아래와 같은 형태도 가능합니다.

 

위 Signal에 대한 enum class를 아래와 같이 변경해 보았습니다.

 

 

같은 signal 이라도 그 속성값이 각각 다를 수 있으므로 sealed class인 signal class의 sub class로 각각의 signal을 다르게 정의할 수 있습니다.

 

 

Signal 클래스를 그냥 상속받은 클래스로 사용하면 될 것을 왜 굳이 sealed class로 사용해야 할까요?

컴파일러는 부모클래스를 상속받은 자식 클래스의 유무를 알지 못합니다.

정의한 signal이 어떠한 타입인지 정의한 getSignalType 함수를 통해 알 수 있습니다.

getSignalType 함수는 when 키워드를 사용하는데 selease class가 아닌 일반 클래스를 사용한 경우라면,

더 정의된 자식 클래스가 없음에도 else branch 를 추가하라는 오류를 만들게 됩니다.

 

 

컴파일러가 상속받은 자식 클래스의 종류를 알지 못하기 때문에 else 구문을 추가하라고 나옵니다.

 

단순히, else만 넣으면 해결 되는 문제입니다.

아래 같은 경우는 BClass가 상속받은 클래스임에도 불구하고, Else 구문으로 에러 없이 처리가 됩니다.

 

하지만, BClass에 대한 처리를 하고 있지 않다는 것을 컴파일러는 잡아내지 못하고, else 구문으로 처리되기 때문에 프로그램은 의도치 않는 동작을 하게 될 수 있으며 해당 문제를 잡아내기도 쉽지 않습니다.

 

sealed class는 상속받은 자식클래스의 종류를 컴파일러가 알고 있기 때문에 when 구문에서 else 없이 정의된 모든 class를 처리할 수 있습니다.

 

아래처럼 else는 불필요한 구문으로 컴파일러는 알고 있습니다.

 

서브 클래스가 자유롭게 추가되어야 한다면 sealed class를 사용할 필요는 없습니다.

위와 같이 SignalType에 대해 Periodic 과 Direct 밖에 없다고 제한을 꼭 둬야 하는 목적이 있을 때에 사용하는 게 좋아 보입니다.

 


 

enum class 의 사용 예

직급과 고과를 입력받아, 아래표를 기반으로 인상되는 가격이 얼마인지
알려주는 프로그램을 만들어보면서 Enum에 대해 더 알아 보겠습니다.

분류 C B A S
연구원 100 180 360 600
선임연구원 120 200 400 800
책임연구원 150 300 600 1000

단위 : 만원

 

 

//인터페이스를 선언해서 상속받아 처리한다.
interface Calculable {
    fun calculate(grade: Grade) : Int
}

enum class Salary2022 (
    val position: String
) /*: Calculable*/ {	//Enum class는 추상클래스이기 때문에 추상함수를 선언 및 구현할 수 있다.
    STAFF("연구원") {
        override fun calculate(grade: Grade): Int  = when(grade) {
            Grade.C -> 100
            Grade.B -> 180
            Grade.A -> 360
            Grade.S -> 600
        }
    },
    MANAGER("선임연구원") {
        override fun calculate(grade: Grade): Int  = when(grade) {
            Grade.C -> 120
            Grade.B -> 200
            Grade.A -> 400
            Grade.S -> 800
        }
    },
    DEPUTY("책임연구원") {
        override fun calculate(grade: Grade): Int  = when(grade) {
            Grade.C -> 150
            Grade.B -> 300
            Grade.A -> 600
            Grade.S -> 1000
        }
    };

    abstract fun calculate(grade: Grade) : Int	//추상함수 or 인터페이스를 상속받을 경우 해당 함수 삭제
    
    companion object {
        fun print() {
                enumValues<Salary2022>().map {
                    Grade.values().map { grade ->
                    println("${it.position}의 ${grade.name} 연봉인상액은 ${it.calculate(grade as Grade)} 입니다")
                }
            }
        }
    }
    
    
    //print(Salary2022.DEPUTY.calculate(Grade.A))
    
    Salary2022.print()
    
//output
연구원의 S 연봉인상액은 600 입니다
연구원의 A 연봉인상액은 360 입니다
연구원의 B 연봉인상액은 180 입니다
연구원의 C 연봉인상액은 100 입니다
선임연구원의 S 연봉인상액은 800 입니다
선임연구원의 A 연봉인상액은 400 입니다
선임연구원의 B 연봉인상액은 200 입니다
선임연구원의 C 연봉인상액은 120 입니다
책임연구원의 S 연봉인상액은 1000 입니다
책임연구원의 A 연봉인상액은 600 입니다
책임연구원의 B 연봉인상액은 300 입니다
책임연구원의 C 연봉인상액은 150 입니다
}

 

반응형

'Kotlin' 카테고리의 다른 글

BlogViewer (feat. JSoup Crawling)  (1) 2022.04.03
inline 함수  (0) 2022.04.01
sequence  (0) 2022.03.27
collection  (0) 2022.03.27
reduce, fold  (0) 2022.03.26