004. 코틀린의 enum과 when

코틀린의 enum과 when

이번 포스팅에서는 Java의 switch를 대체할 수 방법에 대해서 알아보자.

enum 클래스

먼저 enum을 이용한 클래스 선언부터 시작해보자.

아래 코드는 Color라를 클래스명으로 색깔들을 나열한 것이다.

1
2
3
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

코틀린에서 enumclass앞에 올때에만 키워드로서의 역할을 하며, 그 외의 경우엔 일반적인 이름으로 사용할 수 있다.

참고 위와 같은 형태를 소프트 키워드(Soft Keywords)라고 한다.

코틀린의 enum도 Java와 마찬가지로 값의 리스트 형태가 아니며, 별도의 프로퍼티나 메소드를 선언해서 사용할 수 있다.

아래 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum class Color(val r: Int, val g: Int, val b: Int) {
RED(255, 0, 0),
ORANGE(255, 165, 0),
YELLOW(255, 255, 0),
GREEN(0, 255, 0),
BLUE(0, 0, 255),
INDIGO(75, 0, 130),
VIOLET(238, 130, 238);

fun rgb() = (r * 256 + g) * 256 + b
}

/* 출력 결과 */
>>> println(Color.BLUE.rgb())
255

enum 형태의 상수는 일반적인 클래스처럼 생성자 및 프로퍼티 관련 메소드를 사용할 수 있으며, 각 상수에 대해 선언할 때는 해당 상수에 대한 값을 초기화해주어야 한다.

위의 rgb() 메소드처럼, enum 클래스 내부에 메소드를 정의하는 경우 상수와 메소드 사이에 ;를 이용해 구분해줘야 한다.

when에서의 enum 클래스 활용

무지개의 색들을 기억하는가? 무지개 색의 앞 글자를 따면 ROYGBIV 가 된다.

이를 쉽게 외우기 위해 아이들은 Richard Of York Gave Battle In Vain! 이라는 말을 만들어서 쓰곤 한다.

참고 앞 글자를 따면 Red, Orage, Yellow, Green, Blue, Indigo, Violet 이 된다.
우리나라로치면 빨간색, 주황색, 노란색, 초록색, 파란색, 남색, 보라색을 빨주노초파남보 라고 줄여 쓰는 것을 생각하면 된다.

이 무지개색을 enumwhen을 이용해서 구분해보는 코드를 작성해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 사족, Mnemonic는 기억술이라는 뜻을 가진 단어다.
fun getMnemonic(color: Color) =
when (color) {
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}

/* 출력 결과 */
>>> println(getMnemonic(Color.BLUE))
Battle

위의 코드는 파라미터로 넘겨진 값에 해당하는 분기를 찾아서 반환한다.

Java와는 다르게 각 분기에 break를 사용할 필요가 없고, 일치한 분기가 있으면 그 뒤에 있는 분기는 실행되지 않는다.

참고 break의 누락, 혹은 여러 케이스를 묶어서 처리하다가 실수하는 경우 등을 생각해보자. 코틀린은 Java에서 발생하는 버그의 원인을 철저히 제거하고자 한다.

만약 여러 분기를 묶어서 처리해야 하는 경우 아래와 같이 ,를 통해 결합할 수 있다.

1
2
3
4
5
6
7
8
9
fun getWarmth(color: Color) = when(color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}

/* 출력 결과 */
>>> println(getWarmth(Color.ORANGE))
warm

아래 코드와 같이 import를 통해 단순화하여 사용할 수 도 있다.

1
2
3
4
5
6
7
8
import package.path.Color
import package.path.Color.*

fun getWarmth(color: Color) = when(color) {
RED, ORANGE, YELLOW -> "warm"
GREEN -> "neutral"
BLUE, INDIGO, VIOLET -> "cold"
}

when에서의 임의 객체 활용

이제 when에 대해서 알아보자.

코틀린의 when은 Java의 switch보다 강력한 기능을 제공한다.

Java에서는 switch의 조건문으로 상수나 리터럴 값만 사용할 수 있지만, 코틀린은 객체도 조건문으로 사용할 수 있다.

아래 코드를 통해 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color")
}

/* 출력 결과 */
>>> println(mix(BLUE, YELLOW))
GREEN

위의 setOf() 메소드는 파라미터로 넘겨받은 객체들을 묶어 하나의 집합인 Set 객체로 돌려준다.

위의 코드에선 Set<Color> 타입으로 반환되고, 이를 통해 Set<Color> 객체끼리의 비교도 가능해진다.

참고 stdlib.kotlin.collections#setOf 문서

파라미터가 필요없는 when

1
2
3
4
5
6
7
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color")
}

이 코드도 물론 간결하지만, 다소 비효율적이게도 setOf() 메소드를 반복적으로 호출하고 있다는 걸 알 수 있다.

즉, 비교 한 번을 위해 불필요한 객체가 최대 4개까지 만들어질 수 있는 구조인 것이다.

이를 해결하기 위해 파라미터 없이 when을 사용할 수 있다.

일종의 등가교환으로, 가독성을 포기하고 성능을 선택하는 기법이다.

1
2
3
4
5
6
7
8
9
10
11
fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) -> ORANGE
(c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) -> GREEN
(c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) -> INDIGO
else -> throw Exception("Dirty color")
}

/* 출력 결과 */
>>> println(mixOptimized(BLUE, YELLOW))
GREEN

만약 파라미터없이 when이 작성된 경우 모든 분기 조건은 부울 형태(true or false)로 간주된다.

즉 위의 mixOptimized() 메소드는 mix() 메소드와 동일한 역할을 수행하게 되다.

스마트 캐스트

이번엔 (1 + 2) + 4와 같은 간단한 산술식을 표현하는 코드를 작성해보자.

여기서 산술식은 덧셈만 가능하다고 가정한다.

이때 두 개의 숫자를 더해서 하나의 숫자로 표현 하므로 이진트리 형태로 표현할 수 있음을 알 수 있다.

먼저 Num 이라는 클래스를 선언하여 하나의 숫자를 나타내도록 하자.

그 다음 Sum이라는 클래스를 선언하여 두 개의 숫자의 합을 나타내도록 하자.

다시 두 개의 숫자를 더해서 하나의 숫자로 표현 을 재정의해보면 Sum=(Num(x) + Num(y))와 같다.

위의 예로 든 수식 (1 + 2) + 4에도 똑같이 적용해보면 Sum(Sum(Num(1), Num(2)), Num(4))이 될 것이다.

마지막으로 공통 분모인 숫자를 나타내기 위해 Expr 인터페이스를 사용한다고 가정하여 아래와 같은 코드를 작성하였다.

1
2
3
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

이제 위의 인터페이스와 클래스가 구현되었다고 가정할 때 우리는 아래와 출력 결과를 기대할 것이다.

1
2
3
/* 출력 결과 */
>>> println (eval(Sum(Sum(Num(1), Num(2)), Num (4))))
7

원하는 출력 결과인 7을 반환하기 위해 eval() 메소드를 작성해보자.

eval()은 아래 세 가지 조건을 충족해야 한다.

  1. Expr을 구현한 클래스는 NumSum 두 가지이므로 eval() 메소드는 이 두 가지 클래스에 대해 분기해서 동작해야 한다.

  2. Num인 경우엔 파라미터로 받은 값을 반환하도록 동작해야 한다.

  3. Sum인 경우엔 피연산자인 두 Expr을 더한 뒤 반환하도록 동작해야 한다.

먼저 Java 방식으로 작성해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun eval(e: Expr): Int {
if (e is Num) {
val n = e as Num
return n.value
}
if (e is Sum) {
return eval(e.right) + eval(e.left)
}
throw IllegalArgumentException("Unknown expression")
}

/* 출력 결과 */
>>> println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
7

Java의 instanceOf와 유사한 방식으로 코틀린에서는 is 키워드를 사용해 해당 변수가 특정 타입인지 검증할 수 있다.

하지만, Java에선 검증하려는 변수가 특정 타입을 가지고 있고, 해당 타입의 멤버에 접근하는 경우에 명시적인 캐스팅을 진행해주어야 한다.

아래 코드를 통해 Java에서의 방식을 눈으로 확인해보자.

1
2
3
4
5
6
7
class SpecificType {
public int access = 0;
}

if (obj instanceof SpecificType) {
int access = ((SpecificType)obj).access;
}

코틀린은 is를 통해 검증한 타입의 경우 명시적인 캐스팅없이 바로 멤버에 접근할 수 있게 해준다.

이번에도 아래 코드를 통해 Kotlin에서의 방식을 확인해보자.

1
2
3
4
5
6
7
class SpecificType {
val access: Int = 0
}

if (obj is SpecificType) {
val access: Int = obj.access
}

위와 같이 타입 검증 이후 사용자의 명시적인 캐스팅없이 컴파일러가 캐스팅을 해주는 것을 스마트 캐스트 라고 한다.

만약 특정 유형의 명시적인 캐스팅이 필요한 경우 is가 아닌 as를 사용한다.

1
val n = e as Num

if문을 when으로 변경하기

이번엔 Java의 if문을 코틀린의 when으로 변경해보자.

1
2
3
4
5
6
7
8
9
10
11
12
fun eval(e: Expr): Int =
if (e is Num) {
e.value
} else if (e is Sum) {
eval(e.right) + eval(e.left)
} else {
throw IllegalArgumentException("Unknown expression")
}

/* 출력 결과 */
>>> println(eval(Sum(Num(1), Num(2))))
3

분기당 하나의 식만 존재하므로 중괄호는 생략 가능하다.

이제 ifwhen으로 교체해보자.

1
2
3
4
5
6
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval(e.right) + eval(e.left)
else -> throw IllegalArgumentException("Unknown expression")
}

위의 코드에서도 스마트 캐스트를 통해 특정 타입으로의 캐스팅없이 value, right, left에 접근하는 것을 볼 수 있다.

위와 같이 단순한 반환으로 그치지않고, 좀 더 복잡한 식을 분기하는 경우 블록을 사용해서 처리할 수 있다.

if문과 when에서의 코드블록 사용

ifwhen 둘 다 분기마다 블록을 사용할 수 있다.

이 경우 해당 블록의 가장 마지막 코드가 반환값으로 사용된다.

eval() 메소드에 로깅 기능을 추가한 evalWithLogging() 코드를 작성해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun evalWithLogging(e: Expr): Int =
when (e) {
is Num -> {
println("num: ${e.value}")
e.value // 반환되는 값
}
is Sum -> {
val left = evalWithLogging(e.left)
val right = evalWithLogging(e.right)
println("sum: $left + $right")
left + right // 반환되는 값
}
else -> throw IllegalArgumentException("Unknown expression")
}

이제 최초에 언급한 수식인 (1 + 2) + 4evalWithLogging() 메소드를 이용해 호출하면 아래와 같은 결과가 출력된다.

1
2
3
4
5
6
7
>>> println(evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))))
num: 1
num: 2
sum: 1 + 2
num: 4
sum: 3 + 4
7