[자바] 12. switch : 표현식으로 다시 태어난 switch (Java 14)

자바 버전 여행 12편 - 표현식으로 다시 태어난 switch

작성자

카테고리:

* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

(자바 버전 여행 ⑫)

이제부터 자바 12~17은 문법을 손보는 구간이다. record도 텍스트 블록도 이때 들어온다. 그 시작이 오래된 골칫거리 switch다. 자바 14에서 표현식으로 다시 태어났다.

기존 switch의 세 가지 함정

  • break를 빠뜨리면 다음 case로 줄줄 흘러간다(fall-through).
  • 값을 못 낸다. 결과를 담으려면 바깥에 변수를 두고 case마다 대입해야 한다.
  • case마다 break;가 반복돼 코드가 길어진다.
// Before — 값을 담을 변수 + break 반복
int days;
switch (month) {
    case 1: case 3: case 5: case 7: case 8: case 10: case 12:
        days = 31; break;
    case 4: case 6: case 9: case 11:
        days = 30; break;
    default:
        days = 28;
}

fall-through 버그, 직접 재현해 보기

말로만 들으면 실감이 안 난다. break 하나 빠진 코드를 통째로 돌려 보자. 2월을 넣었으니 당연히 “겨울”이 나와야 한다.

public class FallThroughBug {
    public static void main(String[] args) {
        int month = 2;
        String season;
        switch (month) {
            case 12: case 1: case 2:
                season = "겨울";
                // break;  ← 여기서 깜빡했다
            case 3: case 4: case 5:
                season = "봄";
                break;
            default:
                season = "그 외";
        }
        System.out.println(month + "월은 " + season);
    }
}
2월은 봄

2월이 봄이 됐다. “겨울”을 대입해 놓고도 break가 없어 아래 case까지 흘러가 “봄”으로 덮어쓴 것. 컴파일 에러도 경고다운 경고도 없이 조용히 틀린 값이 나온다는 게 이 버그의 무서운 점이다.

같은 코드를 switch case 화살표 문법으로 바꾸면 이렇다.

public class FallThroughFixed {
    public static void main(String[] args) {
        int month = 2;
        String season = switch (month) {
            case 12, 1, 2 -> "겨울";
            case 3, 4, 5  -> "봄";
            default       -> "그 외";
        };
        System.out.println(month + "월은 " + season);
    }
}
2월은 겨울

화살표 오른쪽 하나만 실행되고 바로 끝나니, 흘러갈 곳 자체가 없다. break를 “잘 챙기는” 코드가 아니라 break가 “필요 없는” 구조로 바뀐 거라, 사람이 실수할 여지가 사라진다.

fall-through는 C에서 물려받은 동작이다. 라벨 여러 개를 한 처리로 묶으려고 가끔 의도적으로 쓰기도 했지만, 화살표 문법에선 콤마 라벨(case 1, 2, 3 ->)이 그 자리를 깔끔하게 대신한다.

화살표로, 그리고 값으로

자바 14의 switch는 ->를 쓰고, 그 자체가 값을 반환한다. break도 fall-through도 없다. 여러 라벨은 콤마로 묶는다.

// After — switch가 값을 돌려준다
int days = switch (month) {
    case 1, 3, 5, 7, 8, 10, 12 -> 31;
    case 4, 6, 9, 11           -> 30;
    default                    -> 28;
};

블록이 필요하면 중괄호를 열고, 반환값은 yield로 낸다.

String grade = switch (score / 10) {
    case 10, 9 -> "A";
    case 8     -> "B";
    default    -> {
        System.out.println("낮은 점수: " + score);
        yield "C";
    }
};

직접 해보기 — 옛 switch를 표현식으로

가장 실감 나는 건 직접 바꿔 보는 것이다.

  1. 요일 문자열을 받아 “평일”/”주말”을 돌려주는 코드를 옛 switch로 짠다(break 포함).
  2. 그걸 화살표 switch 표현식으로 바꿔 변수 하나에 담는다.
  3. 일부러 라벨 하나를 빼먹고, 옛 switch에선 조용히 통과되지만 표현식에선 어떻게 되는지 비교한다.
String kind = switch (day) {
    case "토", "일" -> "주말";
    case "월", "화", "수", "목", "금" -> "평일";
    default -> throw new IllegalArgumentException("요일 아님: " + day);
};
System.out.println(day + " → " + kind);

실행해 보면 이렇게 나온다.

토 → 주말

여기서 조금씩 바꿔 보자.

  • default를 지워 보고, 컴파일러가 “모든 경우를 다루지 않았다”고 잡는지 본다.
  • 블록 { ... yield ... }를 하나 넣어, 로그를 찍으면서 값을 반환해 본다.

한 걸음 더 — 열거형(enum)을 switch에 넣으면 default 없이도 컴파일러가 완전성을 검사한다. enum으로 바꿔 확인해 보자.

default를 지우면 어떤 에러가 나오는지, enum 완전성 검사가 실제로 어떤 모습인지는 아래 망라성 검사 절에서 컴파일러 메시지 그대로 보여준다.

✅ 확인 — 화살표 switch가 값을 반환하고 fall-through가 없다는 것, yield가 언제 필요한지 설명할 수 있으면 통과다.

화살표 라벨 — fall-through와 break가 사라진다

새 switch는 case L -> 문법을 쓴다. 화살표 오른쪽만 실행되고 자동으로 끝나 break가 필요 없다. break를 빠뜨려 다음 case로 흘러가는 fall-through 실수가 원천 봉쇄된다. 같은 처리를 하는 여러 라벨은 콤마로 묶는다.

switch (day) {
    case MON, TUE, WED, THU, FRI -> System.out.println("평일");
    case SAT, SUN                -> System.out.println("주말");
}

값을 내는 표현식 — yield

가장 큰 변화는 switch가 값을 낸다는 것이다. 바깥 변수에 case마다 대입하던 걸 한 줄로 줄인다. 간단하면 화살표 오른쪽에 값을, 블록이 필요하면 { … yield 값; }로 낸다. yield는 블록에서 값을 낼 때만 쓴다.

int days = switch (month) {
    case JAN, MAR, MAY, JUL, AUG, OCT, DEC -> 31;
    case APR, JUN, SEP, NOV                -> 30;
    case FEB -> {
        boolean leap = isLeap(year);
        yield leap ? 29 : 28;      // 블록에서 값 내기
    }
};

블록과 yield가 실제로 어떻게 굴러가는지, 점수 배열을 등급으로 바꾸는 완결 예제로 확인해 보자. F 등급만 로그를 남기면서 값도 내야 해서 블록이 필요한 상황이다.

public class GradeCalc {
    public static void main(String[] args) {
        int[] scores = {95, 82, 47};
        for (int score : scores) {
            String grade = switch (score / 10) {
                case 10, 9 -> "A";
                case 8     -> "B";
                case 7     -> "C";
                default    -> {
                    System.out.println("보충 대상: " + score + "점");
                    yield "F";
                }
            };
            System.out.println(score + "점 → " + grade);
        }
    }
}
95점 → A
82점 → B
보충 대상: 47점
47점 → F

47점에서만 블록 안 로그가 먼저 찍히고, yield가 낸 “F”가 grade에 담겼다. return이 아니라 yield를 쓰는 이유도 여기서 드러난다. return은 메서드 자체를 빠져나가 버리지만, yield는 switch 표현식에 값만 돌려주고 다음 문장으로 이어진다.

망라성 검사 — 컴파일러가 빠진 case를 잡는다

표현식 switch는 모든 경우를 다뤄야 한다(exhaustive). 그래서 enum이나 sealed 타입이면 컴파일러가 ‘빠진 case’를 에러로 잡아준다. 새 enum 값을 추가했는데 그걸 안 다룬 switch가 있으면 컴파일이 실패한다 — 런타임에서야 터질 버그가 컴파일 타임으로 앞당겨지는 것이다. 이 안전망이 표현식 switch의 진짜 값어치다.

이것도 눈으로 봐야 믿긴다. 계절 enum을 switch 표현식에 넣은 완결 예제다. default가 없는데도 컴파일이 되는 이유는 네 계절을 전부 다뤘기 때문이다.

public class SeasonSwitch {
    enum Season { SPRING, SUMMER, FALL, WINTER }

    public static void main(String[] args) {
        Season s = Season.FALL;
        String desc = switch (s) {
            case SPRING -> "꽃놀이";
            case SUMMER -> "휴가";
            case FALL   -> "단풍";
            case WINTER -> "눈";
        };
        System.out.println(s + " → " + desc);
    }
}
FALL → 단풍

이제 case WINTER 줄을 지우고 다시 컴파일해 보자. 컴파일러가 바로 막아선다.

SeasonSwitch.java:6: error: the switch expression does not cover all possible input values
        String desc = switch (s) {
                      ^
1 error

“모든 입력을 다루지 않았다”는 에러다. 나중에 누가 Season에 값을 하나 추가해도 그 값을 안 다룬 switch는 전부 이 에러로 걸린다. 런타임에 엉뚱한 값으로 조용히 새는 대신 컴파일 단계에서 잡히는 셈이다.

enum switch 표현식에 습관처럼 default를 붙이면 이 안전망이 꺼진다. 새로 추가된 enum 값이 default로 조용히 흡수돼 버리기 때문. 모든 값을 다뤘다면 default를 일부러 빼는 쪽이 오히려 안전하다.

문장으로 써도 화살표가 낫다

값을 안 내고 부수효과만 있어도 화살표 문법을 쓸 수 있다. 그럼 break 지옥과 fall-through 없이 깔끔하다. 옛 case: 문법도 계속 되지만, 새 코드는 화살표가 기본이다. 그리고 이 표현식 switch는 뒤에 나올 패턴 매칭 switch(21)의 토대이기도 하니, 여기서 익혀두면 그다음이 수월하다.

switch가 표현식이 되면서 뒤에 나올 패턴 매칭의 무대가 깔렸다. 그전에 다음 편은 문자열을 다루는 방식을 바꾼 텍스트 블록이다.

미리 지도를 그려 두면 이렇다. 14편의 record로 데이터가 타입이 되고, 15편의 sealed 클래스와 패턴매칭으로 타입 계층이 닫히면, 18편의 switch 패턴매칭(Java 21)에서 이 셋이 한 문법으로 합쳐진다. 그때의 switch는 값이 아니라 타입과 구조로 분기하는데, 오늘 익힌 Java switch 표현식의 화살표와 망라성 검사가 그대로 그 토대가 된다.

자주 묻는 질문 (FAQ)

switch 표현식은 break가 필요 없나요?

네. 화살표(->) 문법은 fall-through가 없어 break가 필요 없고, 그 자체가 값을 반환합니다.

yield는 언제 쓰나요?

case 본문을 중괄호 블록으로 쓸 때, 그 블록에서 값을 반환하려면 yield를 씁니다. 화살표 오른쪽이 단일 식이면 yield 없이 그 값이 그대로 반환됩니다.

switch 표현식은 자바 몇 버전부터 쓸 수 있나요?

Java 12~13에서 프리뷰로 실험을 거쳐 Java 14(JEP 361)에서 정식 기능이 됐습니다. Java 14 이상이면 별도 옵션 없이 바로 쓸 수 있습니다.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다