지난 편에서는 변수를 프로퍼티로 승격시키는 클래스 선언과 커스텀 접근자, 그리고 패키지·디렉터리 구조의 자유로움까지 살펴봤습니다.
https://dmoritle.tistory.com/287
[Kotlin] 코틀린 온보딩 3편 - 클래스와 프로퍼티: 커스텀 접근자로 이해하기
지난 편에서는 val/var로 변수를 선언하는 방법과 문자열 템플릿을 살펴봤습니다.https://dmoritle.tistory.com/281 [Kotlin] 코틀린 온보딩 2편 - 변수: val/var와 문자열 템플릿으로 이해하기지난 편에서는 코
dmoritle.tistory.com
이번 편의 핵심은 1편에서 잠깐 다루고 넘어간 when의 완전성 검사를 enum과 함께 다시 짚어보고, when이 자바의 switch보다 훨씬 넓게 쓰이는 이유를 살펴보는 것입니다.
enum 클래스와 enum 상수
코틀린도 자바처럼 enum class로 열거형을 선언합니다.
enum class Direction {
NORTH, SOUTH, EAST, WEST
}
자바의 enum과 마찬가지로, 상수마다 생성자 인자를 가질 수도 있습니다.
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
fun main() {
println(Color.RED.rgb) // 16711680
}
여기서 한 가지 문법적 규칙이 있습니다. 상수 목록 뒤에 메서드나 프로퍼티 같은 추가 코드가 온다면, 상수 목록의 마지막에 세미콜론(;)을 반드시 붙여야 합니다.
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF); // 세미콜론 필수
fun printHex() = println("#${rgb.toString(16)}")
}
상수만 나열하고 끝나는 경우에는(위 Direction처럼) 세미콜론이 필요 없습니다.
자바 개발자가 걸려 넘어지는 포인트: 코틀린에서는 이 세미콜론을 깜빡하기 쉽습니다. 자바 enum은 상수 목록 뒤에 세미콜론이 항상 필요하지만, 코틀린은 상수만 나열할 때는 세미콜론이 없어도 되고, 추가 코드가 있을 때만 필요합니다. "코틀린은 세미콜론이 선택"이라는 인상 때문에 이 경우를 놓치면 컴파일 에러를 만납니다.
when으로 enum class 다루기: 완전성 검사 다시 보기
1편에서 when을 식으로 쓸 때는 완전성(exhaustiveness) 검사가 들어간다고 했습니다. 그리고 enum class처럼 컴파일러가 가능한 값을 전부 알고 있는 경우는 else 없이도 완전성을 인정받는다고 짚었습니다. enum class를 직접 다뤄봤으니 이 규칙을 다시 확인해보겠습니다.
enum class Direction { NORTH, SOUTH, EAST, WEST }
fun describe(d: Direction) = when (d) {
Direction.NORTH -> "북쪽"
Direction.SOUTH -> "남쪽"
Direction.EAST -> "동쪽"
Direction.WEST -> "서쪽"
// else 불필요: Direction의 모든 값을 다뤘음을 컴파일러가 확인
}
네 가지 값 중 하나라도 빠뜨리면 컴파일 에러가 납니다.
fun describe(d: Direction) = when (d) {
Direction.NORTH -> "북쪽"
Direction.SOUTH -> "남쪽"
Direction.EAST -> "동쪽"
} // 에러! WEST가 빠져서 완전성 검사 실패

주의할 점: 완전성 검사는
when을 식으로 쓸 때만 강제됩니다. 값을 대입하지 않고 문으로만 쓴다면(예: 분기마다println만 호출하고 끝나는 경우), 값을 빠뜨려도 컴파일은 됩니다. "값을 만들어내야 하는가"가 완전성 검사의 기준이라는 점은 1편에서 다룬 원칙과 그대로 이어집니다.
when의 대상을 변수에 캡처하기
when의 대상이 되는 식이 복잡하다면, when (val 변수 = 식) 형태로 그 결과를 변수에 캡처할 수 있습니다. 이 변수의 범위는 when 블록 안으로 제한됩니다.
fun describe(response: Response) = when (val code = response.status) {
200 -> "성공: $code"
404 -> "찾을 수 없음: $code"
else -> "알 수 없는 상태: $code"
}
response.status를 매번 다시 호출하지 않고, code라는 이름으로 한 번만 평가해서 각 분기에서 재사용하는 것입니다. code는 when 바깥에서는 참조할 수 없습니다.
when 분기 조건에 임의의 객체 사용하기
자바의 switch는 오랫동안 상수(정수, enum, 문자열)만 분기 조건으로 허용했습니다. 코틀린의 when은 그렇지 않습니다. 각 분기 조건은 ==로 비교 가능한 임의의 식이면 됩니다.
fun checkPin(enteredPin: String, storedPin: Int) = when (enteredPin.toInt()) {
storedPin -> "PIN이 일치합니다" // storedPin은 그냥 변수, 상수가 아님
else -> "PIN이 일치하지 않습니다"
}
여기서 storedPin -> ...은 "이 값이면 참"이 아니라, when의 대상(enteredPin.toInt())과 storedPin을 ==로 비교한다는 뜻입니다. 분기 조건 자리에는 상수뿐 아니라 변수든 함수 호출 결과든, 대상과 == 비교가 가능한 식이면 무엇이든 올 수 있습니다.
in, is도 분기 조건으로 쓸 수 있습니다.
fun describe(x: Any) = when (x) {
in 1..10 -> "1~10 사이의 값" // 범위 검사
is String -> "문자열: $x" // 타입 검사
else -> "기타"
}
in은 범위나 컬렉션에 값이 포함되는지, is는 타입 검사를 그대로 분기 조건에 씁니다. 자바 switch의 case 라벨이 컴파일 타임 상수여야 했던 것과 비교하면, 코틀린 when은 런타임에 평가되는 임의의 조건식을 받는다는 점에서 근본적으로 다릅니다.
인자 없는 when 사용하기
when에 검사할 대상을 아예 주지 않을 수도 있습니다. 이때 각 분기 조건은 Boolean을 반환하는 임의의 식이 되고, 참인 조건을 만나는 순서대로 실행됩니다.
fun describe(x: Int) = when {
x < 0 -> "음수"
x == 0 -> "0"
x < 100 -> "100 미만의 양수"
else -> "100 이상"
}
주의할 점: 대상이 있는
when은 enum처럼 컴파일러가 완전성을 확인할 수 있으면else를 생략할 수 있었지만, 대상이 없는when은 항상else가 필수입니다. 조건이 모두Boolean식이라 컴파일러가 "모든 경우가 다뤄졌다"고 판단할 방법이 없기 때문입니다.
if를 when으로 바꿔보기
인자 없는 when은 사실상 if-else if 체인과 같은 일을 합니다. 조건이 세 개 이상으로 늘어나면, if-else if보다 when으로 쓰는 편이 자바 개발자 눈에도 더 읽기 좋습니다.
// if-else if 체인
fun grade(score: Int): String {
return if (score >= 90) {
"A"
} else if (score >= 80) {
"B"
} else if (score >= 70) {
"C"
} else {
"F"
}
}
// 인자 없는 when으로 치환
fun grade(score: Int): String = when {
score >= 90 -> "A"
score >= 80 -> "B"
score >= 70 -> "C"
else -> "F"
}
두 코드는 동작이 완전히 같지만, when은 각 조건과 결과가 한 줄로 나란히 정렬되어 분기가 많아질수록 가독성 차이가 커집니다. 1편에서 다룬 "블록의 값 = 마지막 식" 규칙은 when의 각 분기(-> 뒤)에도 그대로 적용되니, 분기 하나가 여러 줄로 늘어나도 걱정 없습니다.
스마트 캐스트: is 검사 뒤에는 캐스팅이 필요 없다
when이나 if에서 is로 타입을 검사하고 나면, 그 분기 안에서는 컴파일러가 자동으로 해당 타입으로 캐스팅해줍니다. 이를 스마트 캐스트라고 부릅니다.
fun describeAny(x: Any): String = when (x) {
is String -> "길이 ${x.length}인 문자열" // 캐스팅 없이 x.length 접근
is Int -> "정수: ${x + 1}" // 캐스팅 없이 x + 1 연산
else -> "알 수 없는 타입"
}
자바였다면 instanceof로 검사한 뒤에도 ((String) x).length()처럼 명시적 캐스팅이 필요했습니다. 코틀린은 is 검사가 성공한 분기 안에서는 캐스팅 없이 그 타입의 멤버에 바로 접근할 수 있습니다.

주의할 점: 스마트 캐스트는 컴파일러가 "검사한 이후로 값이 바뀌지 않았다"고 보장할 수 있을 때만 동작합니다.
val지역 변수라면 대부분 보장되지만,var프로퍼티는 언제든 다른 코드가 값을 바꿀 수 있어 스마트 캐스트가 전혀 적용되지 않습니다.val프로퍼티라도open이거나 커스텀get()을 가지고 있다면(호출마다 다른 값을 반환할 수 있으므로) 마찬가지로 적용되지 않고, 이때는 명시적 캐스팅(as)이 필요합니다. (open은 상속을 다루는 편에서 자세히 살펴볼 텐데, 지금은 "자식 클래스가 재정의할 수 있도록 열어둔 프로퍼티"라서 언제든 값이 달라질 수 있다는 정도만 알아두면 충분합니다.)
참고: 자바도 Java 16(JEP 394)부터
if (obj instanceof String s)처럼instanceof패턴 매칭을 지원합니다. 다만 새 변수(s)를 직접 선언해야 한다는 점에서, 원래 변수를 그대로 재사용하는 코틀린의 스마트 캐스트와는 다릅니다.
정리
| 구분 | 설명 |
| enum class | 상수 목록 + (선택적으로) 생성자, 메서드. 추가 코드가 있으면 상수 목록 뒤에 세미콜론 필수 |
| when + enum | enum의 모든 값을 다루면 else 없이도 완전성 검사 통과 |
| when (val x = ...) | when의 대상을 변수로 캡처, 범위는 when 내부로 제한 |
| when 분기 조건 | 상수뿐 아니라 임의의 식, in, is까지 가능 (== 비교) |
| 인자 없는 when | 각 분기가 Boolean 식, if-else if 체인 대체 |
| 스마트 캐스트 | is 검사 통과 분기에서 캐스팅 없이 해당 타입 멤버 접근 |
마무리
enum class와 when을 엮어서 완전성 검사를 다시 짚어보고, when이 자바의 switch보다 훨씬 유연하게 쓰이는 지점들(임의 조건, 캡처, 인자 없는 형태)까지 살펴봤습니다. 다음 편에서는 여전히 문으로만 남아있는 for/while 루프와, 예외를 던지고 잡는 방법을 다뤄보겠습니다.
https://dmoritle.tistory.com/289
[Kotlin] 코틀린 온보딩 5편 - 루프와 예외 처리: for/while과 try-catch
지난 편에서는 enum class와 when을 엮어서 완전성 검사를 다시 짚어보고, when이 자바의 switch보다 훨씬 유연하게 쓰이는 지점들을 살펴봤습니다.https://dmoritle.tistory.com/288?category=1362768 [Kotlin] 코틀린
dmoritle.tistory.com
이 시리즈는 『코틀린 인 액션(Kotlin in Action)』을 참고하여 작성했습니다.
이번 편에서 다룬 내용은 아래 문서로도 검증했습니다.
'Language > Kotlin' 카테고리의 다른 글
| [Kotlin] 코틀린 온보딩 6편 - 함수 호출: 이름 있는 인자부터 infix까지 이해하기 (0) | 2026.09.26 |
|---|---|
| [Kotlin] 코틀린 온보딩 5편 - 루프와 예외 처리: for/while과 try-catch (0) | 2026.09.26 |
| [Kotlin] 코틀린 온보딩 3편 - 클래스와 프로퍼티: 커스텀 접근자로 이해하기 (0) | 2026.09.26 |
| [Kotlin] 코틀린 온보딩 2편 - 변수: val/var와 문자열 템플릿으로 이해하기 (1) | 2026.09.06 |
| [Kotlin] 코틀린 온보딩 1편 - 함수: 문과 식부터 이해하기 (0) | 2026.09.06 |