Enum 클래스(Enum classes)
Enum 클래스(Enum classes)
Enum 클래스는 고정된 값들의 집합을 나타내요. 어떤 값이 미리 정의된 여러 옵션(가능한 상태나 모드 같은) 중 하나가 될 수 있을 때 enum 클래스를 쓰면 됩니다.
Enum 클래스의 각 값을 enum 상수(enum constant) 라고 불러요. enum 상수는 enum 클래스 타입의 싱글턴 객체처럼 동작해서, 속성과 함수, 사용자 정의 동작을 가질 수 있어요. 모든 가능한 값을 미리 알고 있고 그 구조가 같을 때 enum 클래스가 가장 잘 맞아요. 각 경우마다 서로 다른 데이터나 다른 구조를 담아야 한다면 sealed 클래스나 인터페이스를 사용하세요.
출처: Kotlin 공식 문서
본문
Enum 클래스 선언하기
enum 클래스를 만들려면 enum 키워드를 사용하고, 중괄호로 묶인 본문을 가진 일반적인 클래스 문법을 따르면 돼요. 클래스 본문 안에 enum 상수들을 쉼표로 구분해서 나열합니다:
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
이 예시에서 Direction이 enum 클래스이고, NORTH, SOUTH, WEST, EAST가 enum 상수예요. 관례상 enum 상수는 상수 값을 나타내기 때문에 보통 대문자로 써요.
enum 상수는 enum 클래스 이름 뒤에 상수 이름을 붙여서 접근할 수 있어요:
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
fun main() {
// `Direction.NORTH` is an enum constant of type `Direction`.
val direction: Direction = Direction.NORTH
println(direction)
// NORTH
}
Kotlin의 모든 enum 클래스는 Enum<T> 기반 클래스에서 상속받아요(여기서 T는 enum 클래스 자신). 예를 들어 Direction enum 클래스는 Enum<Direction>에서 상속받죠. 그래서 enum 상수에는 name과 ordinal 같은 내장 속성이 있어요.
Enum 상수 다루기
enum 상수는 값이기 때문에 변수에 할당하거나, 출력하거나, 함수에 넘기거나, 비교하거나, when 표현식에서 쓸 수 있어요.
Enum 상수 선언하기
enum 상수를 선언하려면 먼저 enum 클래스 생성자에 속성을 정의하고, 각 enum 상수에 괄호 안에서 값을 넘겨야 해요. 다른 일부 언어와 달리 Kotlin은 RED = "#FF0000" 같은 할당 문법을 쓰지 않습니다.
enum 상수는 어떤 타입이든 연관된 값을 가질 수 있어요. 문자열과 숫자가 흔한 예시이고, Boolean이나 다른 enum 클래스, 사용자 정의 클래스 같은 타입도 쓸 수 있어요. 각 색의 16진수 색상 코드를 저장하는 Color enum 클래스를 생각해 볼게요:
enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF")
}
각 enum 상수에 넘기는 값은 생성자 파라미터 타입과 일치해야 해요. 여기서 hex는 Color enum 클래스의 문자열 속성입니다. 각 enum 상수는 이 속성에 자신만의 문자열 값을 넘겨주죠.
enum 상수에 숫자 값을 연관시킬 수도 있어요. 예를 들어 생성자에 Int 타입을 선언하고 각 enum 상수에 Int 값을 제공하면 됩니다:
enum class Priority(val level: Int) {
LOW(0),
MEDIUM(1),
HIGH(2)
}
Enum 상수와 그 속성에 접근하기
enum 상수는 enum 클래스 이름을 통해 접근할 수 있어요. enum 상수에 연관된 속성에 접근할 때는 color.hex나 Color.GREEN.hex처럼 점 표기를 사용합니다:
enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF")
}
fun main() {
val color: Color = Color.RED
println(color)
// RED
println(color.hex)
// #FF0000
println(Color.GREEN.hex)
// #00FF00
}
여기서 Color.RED는 Color 타입의 enum 상수예요. color 변수는 이 enum 상수를 저장하죠.
직접 정의한 속성 외에도, 모든 enum 상수는 자신의 이름과 enum 클래스 선언에서의 위치(0부터 시작)를 가져오는 내장 속성 name과 ordinal을 갖고 있어요:
enum class RGB { RED, GREEN, BLUE }
fun main() {
println(RGB.RED.name)
// RED
println(RGB.RED.ordinal)
// 0
}
Enum 상수를 함수에 넘기기
enum 상수는 값이기 때문에 함수에 넘길 수 있어요. 그러면 함수가 enum 클래스에 정의된 고정된 옵션 집합만 받게 되어 타입 안전한 코드가 됩니다:
enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF")
}
//sampleStart
fun printColor(color: Color) {
println("Color: $color")
println("Hex code: ${color.hex}")
}
fun main() {
printColor(Color.BLUE)
// Color: BLUE
// Hex code: #0000FF
}
//sampleEnd
여기서 printColor() 함수는 Color 타입의 값을 받으므로, 어떤 Color enum 상수든 넘길 수 있어요.
enum 상수는 싱글턴 객체처럼 동작하지만, 컴파일러는 그것들을 enum 클래스 타입의 값으로 취급해요. enum 클래스 이름 자체는 타입으로 쓸 수 있지만, enum 상수는 enum 타입으로 쓸 수 없습니다:
enum class Color {
RED, GREEN, BLUE
}
fun printColor(color: Color) {
println(color)
}
fun printRed(color: Color.RED) {
println(color)
// Error: enum entry cannot be used as a type
}
when 표현식에서 enum 상수 사용하기
각 상수를 개별적으로 처리하고 싶을 때 enum 클래스는 when 표현식과 가장 잘 어울려요:
enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF")
}
//sampleStart
fun describeColor(color: Color): String {
return when (color) {
Color.RED -> "Red is a warm color"
Color.GREEN -> "Green is a natural color"
Color.BLUE -> "Blue is a cool color"
}
}
fun main() {
println(describeColor(Color.RED))
// Red is a warm color
}
//sampleEnd
when 표현식에서 모든 enum 상수를 사용하면 else 분기가 필요 없어요.
Enum 상수 찾기
때로는 문자열, 인덱스, 또는 연관된 값 중 하나에서 enum 상수를 얻어야 할 때가 있어요. Kotlin은 이름, 위치, 사용자 정의 값으로 상수를 찾는 내장 API를 제공합니다.
예를 들어 각 색이 연관된 RGB 값을 가진 enum 클래스를 생각해 볼게요. 이름으로 enum 상수를 찾으려면 valueOf() 함수를 사용해요:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
fun main() {
val color = Color.valueOf("RED")
println(color)
// RED
}
valueOf()에 넘기는 이름은 enum 상수 이름과 정확히 일치해야 해요. 지정한 이름의 enum 상수가 없으면 valueOf()는 IllegalArgumentException을 던집니다.
enum 선언에서의 위치로 enum 상수를 찾으려면 enum의 entries 속성에 getOrNull() 함수를 사용하세요:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
//sampleStart
fun main() {
val color = Color.entries.getOrNull(0)
println(color)
// RED
}
//sampleEnd
enum 위치는 0부터 시작해요. 이 예시에서 RED는 위치 0, GREEN은 1, BLUE는 2예요. 이 방법은 파일이나 사용자 입력에서 온 정수(enum 상수 위치를 나타내는)가 있을 때 유용합니다. 일부 다른 언어와 달리 Kotlin은 Int를 enum 상수로 직접 캐스팅하게 하지 않아요. 대신 정수를 인덱스로 써서 entries.getOrNull(index)로 상수를 찾으세요.
만약 그 정수가 enum 상수를 재정렬해도 안정적으로 유지되어야 하는 값을 나타낸다면, rgb나 code 같은 명시적 숫자 속성을 정의하고 일치하는 값을 가진 상수를 검색하세요.
entries는 특화된 List이기 때문에 표준 컬렉션 API를 쓸 수 있어요. 예를 들어 연관된 값으로 enum 상수를 찾으려면 first()로 entries를 검색하면 됩니다:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
//sampleStart
fun main() {
val color = Color.entries.first { it.rgb == 0xFF0000 }
println(color)
// RED
}
//sampleEnd
일치하는 상수가 없으면 first()는 NoSuchElementException을 던져요. 대신 null을 얻으려면 firstOrNull()을 사용하세요.
enum 상수의 개수를 얻으려면 size 속성을 사용해요. 예를 들어:
enum class RGB { RED, GREEN, BLUE }
fun main() {
println(RGB.entries)
// [RED, GREEN, BLUE]
println(RGB.entries.size)
// 3
println("The first color is: ${RGB.valueOf("RED")}")
// "The first color is: RED"
}
이름, 위치, 연관된 값으로 enum 상수를 자주 찾아야 한다면 companion object에 헬퍼 함수를 추가하세요:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF);
companion object {
fun fromName(name: String): Color? =
entries.find { it.name == name }
fun fromPosition(position: Int): Color? =
entries.getOrNull(position)
fun fromRgb(rgb: Int): Color? =
entries.find { it.rgb == rgb }
}
}
fun main() {
println(Color.fromName("RED"))
// RED
println(Color.fromPosition(1))
// GREEN
println(Color.fromRgb(0x0000FF))
// BLUE
println(Color.fromRgb(0xABCDEF))
// null
}
companion object 헬퍼 함수는 예외를 던지는 대신 null을 반환하는 안전한 조회가 필요할 때 유용해요.
위에서 쓴 entries나 valueOf() 같은 조회 API는 합성 멤버(synthetic member) 의 예시예요. 여기서 합성이라는 말은, Kotlin이 선언하지 않았음에도 이 멤버들을 자동으로 제공한다는 뜻입니다. 그래서 모든 enum 클래스는 추가 코드를 작성하지 않아도 entries 속성으로 상수를 나열하고 valueOf() 함수로 이름으로 상수를 얻을 수 있어요.
enumEntries<T>()나 enumValueOf<T>() 같은 제네릭 헬퍼 함수로 enum 클래스의 상수에 접근할 수도 있어요. 이 함수들은 reified 타입 파라미터를 사용합니다. 이런 파라미터는 제네릭 인라인 함수 안에서 실제 enum 타입을 사용할 수 있게 해 줘서, 헬퍼 함수가 enum 타입 T와 직접 동작할 수 있게 하죠:
| 함수 | 설명 |
|---|---|
enumEntries<T>() |
(권장) enum 타입 T의 모든 enum 항목을 반환해요. 매 호출이 같은 리스트를 반환합니다. |
enumValues<T>() |
enum 타입 T의 모든 enum 항목을 담은 배열을 반환해요. 매 enumValues<T>() 호출이 새 배열을 만듭니다. |
enumValueOf<T>() |
이름으로 단일 enum 항목을 반환하고, 일치하는 항목이 없으면 IllegalArgumentException을 던져요. |
예를 들어:
import kotlin.enums.enumEntries
enum class RGB { RED, GREEN, BLUE }
inline fun <reified T : Enum<T>> printAllValues() {
println(enumEntries<T>().joinToString { it.name })
}
inline fun <reified T : Enum<T>> findByName(name: String): T = enumValueOf<T>(name)
fun main() {
printAllValues<RGB>()
// RED, GREEN, BLUE
println(findByName<RGB>("GREEN"))
// GREEN
}
인라인 함수와 reified 타입 파라미터에 대한 자세한 내용은 '인라인 함수(Inline functions)' 문서를 보세요.
Enum 상수 비교와 정렬
enum 상수를 비교하려면 == 구조적 동등성 연산자를 사용해요:
enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF")
}
//sampleStart
fun main() {
val color = Color.RED
println(color == Color.RED)
// true
println(color == Color.BLUE)
// false
}
//sampleEnd
각 enum 상수는 싱글턴 객체처럼 동작하므로, enum 상수를 비교하는 것은 두 값이 같은 상수를 가리키는지 확인하는 일이에요.
모든 enum 클래스는 기본적으로 Comparable 인터페이스를 구현해요. 그래서 enum 상수를 비교하고 정렬할 수 있습니다. 상수는 enum 선언에서의 위치(그 ordinal 값)로 정렬되는데, 이는 먼저 선언된 상수가 가장 작은 것으로 간주된다는 뜻이에요:
enum class Priority {
LOW, MEDIUM, HIGH
}
fun main() {
println(Priority.LOW < Priority.HIGH)
// true
println(Priority.HIGH > Priority.MEDIUM)
// true
}
정렬도 같은 선언 순서를 따릅니다. 예를 들어 entries.sorted()는 이름과 무관하게 선언된 순서대로 상수를 반환해요:
enum class Priority {
HIGH, LOW, MEDIUM
}
fun main() {
println(Priority.entries.sorted())
// [HIGH, LOW, MEDIUM]
}
Enum<T> 클래스는 compareTo(), equals(), hashCode() 함수를 제공하며, 일반 클래스처럼 동작을 커스터마이즈하려고 이것들을 오버라이드할 수는 없어요. 비교는 항상 선언 순서를 따르죠.
다른 순서가 필요하다면 선언 순서에 의존하지 마세요. 대신 명시적 속성을 정의하고 그것으로 정렬해요. 예를 들어 색을 밝기로 정렬해 볼게요:
enum class Color(val brightness: Int) {
RED(1),
GREEN(3),
BLUE(2)
}
fun main() {
println(Color.entries.sortedBy { it.brightness })
// [RED, BLUE, GREEN]
}
자세한 내용은 '순서(Ordering)' 문서를 보세요.
Enum 클래스에 함수 추가하기
속성과 마찬가지로 enum 클래스도 함수를 가질 수 있어요. 모든 enum 상수가 공유하는 함수를 추가하거나, 속성과 결합하거나, 연산자 함수를 정의할 수 있습니다.
모든 상수가 공유하는 함수 추가하기
모든 enum 상수가 공유하는 동작을 추가하려면 enum 클래스 본문에 함수를 정의하면 돼요. enum 클래스에 멤버를 정의한다면 상수 정의와 멤버 정의를 세미콜론으로 구분해야 합니다:
enum class Direction {
NORTH, SOUTH, WEST, EAST;
fun isVertical(): Boolean = this == NORTH || this == SOUTH
}
fun main() {
println(Direction.NORTH.isVertical())
// true
println(Direction.EAST.isVertical())
// false
}
모든 enum 상수는 공유 함수를 호출할 수 있어요. 함수 안에서 this는 호출된 enum 상수를 가리킵니다.
생성자 속성과 함수를 결합해서 각 상수에 데이터를 연관시키고, 그 데이터를 사용하는 동작을 추가할 수도 있어요:
enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF");
fun describe(): String = "$name has hex code $hex"
}
fun main() {
println(Color.RED.describe())
// RED has hex code #FF0000
}
여기서 각 상수는 자신만의 hex 값을 저장하고, 공유 describe() 함수는 내장 name 속성과 hex 속성을 둘 다 사용해요.
연산자 함수 추가하기
enum 클래스는 연산자 함수도 정의할 수 있어요. 그래서 enum 상수를 연산자와 함께 쓸 수 있습니다. 예를 들어 ! 연산자로 반대 방향을 반환하는 not() 연산자 함수를 정의해 볼게요:
enum class Direction {
NORTH, SOUTH, WEST, EAST;
operator fun not(): Direction = when (this) {
NORTH -> SOUTH
SOUTH -> NORTH
WEST -> EAST
EAST -> WEST
}
}
fun main() {
println(!Direction.NORTH)
// SOUTH
}
익명 클래스 사용하기
enum 상수는 대응하는 함수를 가진 자신만의 익명 클래스를 선언할 수 있고, 기반 함수를 오버라이드할 수도 있어요. 익명 클래스를 쓰면 enum 상수 이름 바로 뒤에 클래스 본문을 작성하며, Kotlin이 enum 클래스를 수퍼타입으로 추론합니다.
이는 enum 클래스에 추상 함수를 선언하고 각 상수가 자신만의 구현을 제공하도록 요구할 때 유용해요. 각 상수는 자신의 익명 클래스 안에서 추상 함수를 오버라이드하죠:
enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
fun main() {
var state = ProtocolState.WAITING
println(state)
// WAITING
state = state.signal()
println(state)
// TALKING
}
여기서 각 상수는 추상 signal() 함수를 다르게 구현해요. 그래서 어떤 상수에 따라 signal()을 호출하면 다른 다음 상태가 반환되죠.
enum 상수는 싱글턴 객체처럼 동작하지만, enum 상수의 타입은 자신의 익명 클래스가 아니라 enum 클래스 자체예요. 그래서 익명 클래스 본문 안에 선언한 멤버에는 접근할 수 없습니다:
enum class ProtocolState {
WAITING {
val waitingMessage = "Waiting for a signal"
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
fun main() {
println(ProtocolState.WAITING.waitingMessage)
// Error: unresolved reference 'waitingMessage'
}
모든 상수에 데이터나 동작을 노출하려면 enum 클래스 본문에 선언하세요. 각 상수가 자신만의 구현을 필요로 할 때는 추상 멤버를 사용하면 돼요.
Enum 클래스에서 인터페이스 구현하기
enum 클래스는 인터페이스를 구현할 수 있지만 클래스에서 상속받을 수는 없어요. 모든 enum 상수에 인터페이스 멤버의 공통 구현을 제공하거나, 각 상수가 익명 클래스에서 자신만의 구현을 제공하게 할 수 있습니다.
인터페이스를 구현하려면 enum 클래스 선언에 추가하면 돼요:
import java.util.function.BinaryOperator
import java.util.function.IntBinaryOperator
//sampleStart
enum class IntArithmetics : BinaryOperator<Int>, IntBinaryOperator {
PLUS {
override fun apply(t: Int, u: Int): Int = t + u
},
TIMES {
override fun apply(t: Int, u: Int): Int = t * u
};
override fun applyAsInt(t: Int, u: Int) = apply(t, u)
}
//sampleEnd
fun main() {
val a = 13
val b = 31
for (f in IntArithmetics.entries) {
println("$f($a, $b) = ${f.apply(a, b)}")
}
}
이 예시에서 IntArithmetics enum 클래스는 enum 클래스 선언에서 BinaryOperator<Int>와 IntBinaryOperator 두 인터페이스를 구현해요. PLUS와 TIMES가 apply()에서 하듯, 각 상수는 자신의 익명 클래스 본문 안에서 인터페이스 멤버를 오버라이드할 수 있고, applyAsInt()는 모든 상수에 공통 구현을 제공하죠.
더 알아보기
- sealed 클래스와 인터페이스(Sealed classes and interfaces) — enum과 달리 구조가 다른 각 경우 다루기
- 인라인 값 클래스(Inline value classes)
- 순서(Ordering) —
Comparable과 정렬