Kotlin enum class

홍성덕·2024년 8월 30일

enum class란?

프로그래밍을 하다 보면 데이터에 따라 몇가지로 제한된 값만을 가지는 경우가 존재한다. 예를 들면 요일, 계절, 주사위, 방향(동서남북)은 각각 7개, 4개, 6개, 4개로 한정되어 있으며 개수는 변하지 않는다. 이럴 때 Kotlin의 enum class를 사용하여 제한된 값만을 가지도록 하는 것이 적합하다.

Kotlin의 enum class는 미리 정의된 상수들로 이루어진 제한된 집합을 표현하기 위한 클래스이다. 이러한 상수들은 미리 정의된 값들만 허용하기 때문에 type-safe하게(타입 안전성을 가지도록) 다룰 수 있다. 타입 안전성은 변수나 함수가 잘못된 타입의 값을 사용할 수 없도록 컴파일러가 미리 검증하는 것을 의미한다.

enum class를 사용하지 않고 단순히 상수를 선언해서 사용했다고 가정해보자.

object Dice {
    const val ONE = 1
    const val TWO = 2
    const val THREE = 3
    const val FOUR = 4
    const val FIVE = 5
    const val SIX = 6
}

fun getDiceString(dice: Int): String {
    return when (dice) {
        Dice.ONE -> "One"
        Dice.TWO -> "Two"
        Dice.THREE -> "Three"
        Dice.FOUR -> "Four"
        Dice.FIVE -> "Five"
        Dice.SIX -> "Six"
        else -> "Nothing"
    }
}

fun main() {
	println(getDiceString(dice = 7)) // 출력 : Nothing
}

위 예시를 보면 단순히 상수를 선언하기 위해 object Dice에 1부터 6까지의 상수를 선언해주었다. 그리고 getDiceString()를 통해 적절한 String을 리턴한다. 그런데 일단 getDiceString(dice: Int)에서 인자로 전달되는 dice가 Int 타입이다. enum class로 타입을 제한한 것이 아니기 때문에 파라미터를 Int 타입으로 선언할 수 밖에 없다.

그로 인해 발생하는 문제가, 주사위의 숫자는 6가지 경우의 수밖에 없는데 실제로 작동하는 코드는 예시의 println(getDiceString(dice = 7)) 1~6을 제외한 정수도 전달될 수 있다. 그래서 when 표현식에 else문을 추가하는 불필요한 코드 작성도 발생한다.

object Number {
    const val NUMBER_ONE = 1
}

fun main() {
    println(getDiceString(dice = Number.NUMBER_ONE)) // 출력 : One
}

그리고 이렇게 다른 상수이지만 같은 값을 가지고 있으면 코드가 정상 동작한다는 것도 의도치 않은 결과를 발생시킬 수 있다. 예시에서 NUMBER_ONE 상수는 Dice와 관련된 상수가 아님에도 불구하고 1이라는 정수를 가지고 있기 때문에 getDiceString() 함수에 인자로 전달되면 코드가 정상 동작한다.

그래서 위와 같은 상황들을 방지하고자 enum class를 사용한다.

enum class Dice(val num: Int) {
    ONE(1), TWO(2), THREE(3), FOUR(4), FIVE(5), SIX(6)
}

fun getDiceString(dice: Dice): String {
    return when (dice) {
        Dice.ONE -> "One"
        Dice.TWO -> "Two"
        Dice.THREE -> "Three"
        Dice.FOUR -> "Four"
        Dice.FIVE -> "Five"
        Dice.SIX -> "Six"
    }
}

fun main() {
    println(getDiceString(dice = 7)) // 컴파일 오류
}

Dice를 enum class로 선언하여 1부터 6까지 경우의 수만 가지도록 제한하였다. 그리고 getDiceString(dice: Dice)라는 함수에 Dice 타입을 파라미터로 선언할 수 있게 되었다. 그래서 아까처럼 Int 값을 전달하면 타입 불일치로 컴파일 오류가 발생한다.

그리고 enum class로 선언하면 컴파일러가 해당 enum class의 모든 가능한 값을 알고 있게 된다는 장점이 있다. 예시를 통해 설명하자면, 컴파일러가 이미 Dice가 1~6을 가진다는 것을 알고 있기 때문에 when 표현식을 사용할 때 else 문을 추가할 필요가 없다.
그리고 모든 가능한 값을 알고 있기 때문에, 컴파일러는 모든 경우의 수에 대응하도록 강제한다.

fun getDiceString(dice: Dice): String {
    return when (dice) {
        Dice.ONE -> "One"
        Dice.TWO -> "Two"
        Dice.THREE -> "Three"
    }
}

만약 위와 같이 코드를 작성하면

'when' expression must be exhaustive, add necessary 'FOUR', 'FIVE', 'SIX' branches or 'else' branch instead

이러한 컴파일 오류가 발생한다. when 표현식은 철저해야 하기 때문에 'FOUR', 'FIVE', 'SIX' 브랜치를 추가하거나 else 브랜치를 추가하라는 내용이다.

이렇게 enum class를 사용하면 값을 제한하면서도 모든 경우의 수에 대응하도록 안전하게 코드를 작성할 수 있다.


enum class의 상속

1. 다른 클래스가 enum class의 하위 클래스가 될 수 있는가?

불가능하다. enum class는 생성자가 private이기 때문이다.

public enum Dice {
   private final int number;
   ONE(1),
   TWO(2),
   THREE(3),
   FOUR(4),
   FIVE(5),
   SIX(6);
   
   // ...

   private Dice(int number) {
      this.number = number;
   }
   
   // ...
}

위의 코드는 enum class를 Decompile한 코드이다. 생성자가 private으로 선언되어 있는 것을 확인할 수 있다. 그래서 class SubClass : Dice() 이렇게 코드를 작성하면 컴파일 오류가 발생한다. 생성자가 private이기 때문에 하위 클래스에서 enum class의 생성자를 호출할 수 없기 때문이다.
덧붙이자면, 생성자가 private이기 때문에 enum class의 내부에서만 생성자에 접근이 가능하다. 그래서 enum class 내부에서는 ONE(1), TWO(2)과 같이 정의가 가능한 것이다. 그리고 이를 통해 우리는 각 enum constant는 해당 enum class의 인스턴스라는 것도 알 수 있다.

2. 다른 클래스가 enum class의 상위 클래스가 될 수 있는가?

enum class가 클래스를 상속하는 것은 불가능하다. 즉 어떤 클래스의 하위 클래스가 되는 것은 허용되지 않는다. 하지만 interface를 구현하는 것은 가능하다.

// interface를 구현하는 것만 가능하다.
enum class Dice(val number: Int) : Interface1, Interface2 {
    ONE(1), TWO(2), THREE(3), FOUR(4), FIVE(5), SIX(6)
}

interface Interface1
interface Interface2

enum constant는 싱글 객체(싱글 인스턴스)

fun main() {
    val dice1 = Dice.ONE
    val dice2 = Dice.ONE

    println(dice1 === dice2) // 출력: true
}

enum class의 각 enum constant(상수)는 해당 enum class의 인스턴스이자 싱글 인스턴스이다. 즉 enum constant는 각각 유일하고 고유한 인스턴스로 애플리케이션 어디에서나 같은 객체로 참조된다. 위의 코드를 보면 dice1dice2는 동일한 인스턴스인 것을 알 수 있다.

enum 상수들은 컴파일 시점에 이미 값이 결정되고 enum 클래스가 처음 사용되어 클래스가 (메모리에) 로드될 때 enum 상수가 싱글 인스턴스(유일한 인스턴스)로 생성된다.

enum class Color {
    RED, GREEN, BLUE
}

예를 들어, Color 클래스가 처음 사용되어 로드될 때, RED, GREEN, BLUE 상수 각각이 해당 enum 클래스의 유일한 인스턴스로 생성된다. 컴파일 타임에는 상수들의 정의가 결정되고, 런타임에 클래스가 로드되면서 실제 객체들이 생성되는 것이다. 이렇게 생성된 enum 상수들은 프로그램 실행 중 한 번만 생성되며, 이후에는 항상 같은 객체를 참조하게 된다.

클래스 로드는 클래스의 바이트코드(.class)를 메모리에 로드하고 실행 가능한 상태로 만드는 것을 말한다.


참고자료

profile
안드로이드 주니어 개발자

0개의 댓글