Java 14

switch-case와 break로 메뉴 선택 흐름 만들기

if로도 조건을 나눌 수 있는데 switch를 왜 쓰고, break가 빠지면 왜 이상하게 다음 case까지 실행되는지 헷갈린다.

근거 · 교안 p103-p108

switch-case와 break로 메뉴 선택 흐름 만들기 대표 이미지

1장: switch-case의 존재 이유: if와 다른 선택의 조건

솔라의 손가락이 노트북 트랙패드 위에서 멈칫했다. 화면에는 솔라가 방금 완성한 작은 메뉴 선택 프로그램 코드가 떠 있었다. 커피 메뉴를 번호로 고르는 간단한 코드였다. 1번은 아메리카노, 2번은 라떼, 3번은 카푸치노. 코드는 잘 돌아갔다. 하지만 솔라의 미간은 펴지지 않았다.

if (menu == 1) {
    System.out.println("아메리카노를 선택하셨습니다.");
} else if (menu == 2) {
    System.out.println("라떼를 선택하셨습니다.");
} else if (menu == 3) {
    System.out.println("카푸치노를 선택하셨습니다.");
} else {
    System.out.println("없는 메뉴입니다.");
}

if, else if, else if… 꼬리처럼 길게 늘어선 구조가 영 마음에 들지 않았다. ‘이럴 때 switch 문을 쓰면 좋다고 했는데.’ 솔라는 강의에서 들었던 말을 떠올렸다. 하나의 값을 여러 경우와 비교할 때 쓴다는 설명이었다.

“결국 똑같은 거 아니야?”

솔라는 혼잣말을 하며 코드를 복사해 아래에 붙여 넣고, switch 문으로 고쳐보기 시작했다.

switch의 괄호 안에는 비교할 변수 menu가 들어가고… case 뒤에는 비교할 값이 오니까… case 1, case 2….”

타자를 치던 솔라의 손가락이 다시 멈췄다. 코드는 분명 더 깔끔해 보였다. 반복되는 menu == 부분이 사라졌으니까. 하지만 근본적인 의문이 사라지지 않았다. 어차피 menu라는 변수 하나를 두고 값이 1일 때, 2일 때, 3일 때를 나누는 건 똑같지 않은가. 왜 굳이 if와 비슷한 역할을 하는 switch라는 문법이 따로 있는 걸까. 그냥 좀 더 보기 편하라고 만든 것뿐일까?

솔라의 고민을 들었는지, 소파에서 책을 읽던 언니 루나가 조용히 다가와 솔라의 노트북 화면을 들여다보았다.

if 문을 switch 문으로 바꾸고 있구나.”

“응. 코드가 더 깔끔해지는 건 알겠는데, 왜 굳이 switch가 필요한지 모르겠어. 어차피 if로도 다 할 수 있는 일이잖아. 그냥 똑같은 기능을 다른 모양으로 만든 것 같아.”

솔라의 말에 루나는 잠시 생각하더니, 솔라가 작성한 if-else if 코드의 한 줄을 가리켰다.

“만약 이 두 번째 조건이 조금 더 복잡해진다면 어떨까?”

루나는 키보드에 손을 얹고 else if (menu == 2) 부분을 살짝 수정했다.

// 루나가 수정한 코드
if (menu == 1) {
    System.out.println("아메리카노를 선택하셨습니다.");
} else if (menu == 2 && isMorning) { // <- 이 부분
    System.out.println("오전 할인 라떼를 선택하셨습니다.");
} else if (menu == 3) {
    System.out.println("카푸치노를 선택하셨습니다.");
} else {
    System.out.println("없는 메뉴입니다.");
}

“예를 들어 ‘오전 시간(isMorning)이면서 동시에 2번을 골랐을 때’만 라떼 할인을 해준다고 해보자. 이 if 문은 이제 switch로 바꿀 수 있을까?”

“어…?”

솔라는 고개를 갸웃하며 루나가 수정한 코드를 switch 문으로 옮기려고 시도했다.

switch (menu)는 똑같고… case 1:도 괜찮지. 그런데 두 번째는… case 2 && isMorning:? 이건 안 되는데.”

솔라는 금방 막다른 길에 다다랐다. case 뒤에는 변수나 수식이 올 수 없었다. 오직 menu 변수와 비교할 ‘값’만 올 수 있었다. isMorning이라는 또 다른 변수의 상태를 함께 확인할 방법이 case 문에는 없었다.

순간, 무언가 머리를 스쳤다.

“아!”

솔라는 작은 탄성을 내뱉었다. if-else ifswitch가 완전히 다른 경기장에서 뛰는 선수라는 느낌이 들었다.

if-else if는 각 줄이 완전히 독립적인 질문을 던질 수 있구나. 첫 줄에서는 ‘menu가 1이야?’라고 묻고, 다음 줄에서는 ‘menu가 2이고 동시에 아침이야?’라고 전혀 다른 조건을 물어볼 수 있어. 그런데 switch는….”

솔라는 자신이 쓰다 만 switch 코드를 바라보았다.

switch (menu) {
    case 1:
        // ...
    case 2:
        // ...
}

switch는 딱 한 가지 질문만 할 수 있는 거네. ‘menu의 값이 뭐야?’라고 처음에 한 번만 묻고, 그 대답이 1일 때, 2일 때, 3일 때로 그냥 점프하는 거구나.”

루나는 조용히 고개를 끄덕였다.

솔라는 다시 맨 처음의 if-else if 코드를 보았다. menu == 1, menu == 2, menu == 3… 모든 조건이 오직 menu라는 단 하나의 변수 값에만 의존하고 있었다. 이런 상황이야말로 switch가 활약할 무대였다. 여러 개의 서로 다른 질문이 아니라, ‘단일 값 분기’ 상황.

“그렇구나. if는 여러 갈래의 길마다 다른 질문을 할 수 있는 유연한 탐험가 같고, switch는 ‘목적지가 1번 방이야? 2번 방이야? 3번 방이야?’ 하고 입구에서 한 번에 확인하고 보내주는 안내원 같아.”

솔라는 이제 switch의 존재 이유를 확실히 알 것 같았다. 단순히 코드가 짧아지는 것 이상의 의미가 있었다. ‘이 코드는 단 하나의 값을 기준으로 경우를 나누고 있습니다’라는 의도를 명확하게 드러내는 장치였던 것이다.

“이제 알겠어. 이런 메뉴 선택은 switch가 훨씬 더 자연스러워.”

자신감을 얻은 솔라는 원래의 메뉴 선택 코드를 switch 문으로 망설임 없이 완성했다. 훨씬 깔끔하고 의도도 명확해 보였다.

int menu = 1;
String food = "";

switch (menu) {
    case 1:
        food = "아메리카노";
    case 2:
        food = "라떼";
    case 3:
        food = "카푸치노";
    default:
        food = "없는 메뉴";
}
System.out.println(food + "를 선택하셨습니다.");

“좋아, 이제 실행해 봐야지! menu가 1이니까 ‘아메리카노’가 나오면 성공이야.”

솔라는 기대하며 실행 버튼을 눌렀다. 잠시 후, 콘솔 창에 찍힌 결과는 솔라의 예상을 완전히 빗나갔다.

없는 메뉴를 선택하셨습니다.

“어? 왜… 아메리카노가 아니고 없는 메뉴가 나오지? case 1로 갔으면 바로 끝나야 하는 거 아니야?”

솔라는 당황했다. switch가 어떤 역할을 하는지는 알았지만, 어떻게 작동하는지는 아직 모르는 모양이었다. 왜 코드는 솔라의 의도대로 멈추지 않고 끝까지 달려가 버린 걸까.

2장: break의 역할: 흐름 제어의 안전장치

솔라의 노트북 화면에는 두 개의 문장이 나란히 떠 있었다. 하나는 솔라가 작성한 코드 속 food = "아메리카노";라는 약속이었고, 다른 하나는 콘솔 창에 출력된 없는 메뉴를 선택하셨습니다.라는 배신이었다. 솔라는 두 문장을 번갈아 보며 눈을 가늘게 떴다. menu 변수의 값은 분명 1이었다. 그렇다면 switch 문은 case 1:로 정확히 찾아갔을 것이다. 그런데 왜 결과는 마지막 default:의 값일까.

솔라는 혹시나 하는 마음에 menu 변수의 값을 2로 바꾸고 다시 실행해 보았다. 결과는 똑같았다. 없는 메뉴를 선택하셨습니다. 이번엔 3으로 바꿔보았다. 결과는 역시나, 없는 메뉴를 선택하셨습니다. 였다. 마치 어떤 번호를 선택하든 모든 코드를 다 훑고 지나가 마지막에 있는 값으로 덮어써 버리는 것 같았다.

“이상해… if문은 조건이 맞으면 자기 블록만 실행하고 끝나는데, switch는 왜 이러지?”

솔라의 혼잣말에, 옆에서 지켜보던 루나가 조용히 화면을 가리켰다.

“코드가 거짓말을 하진 않아. 솔라, 네가 컴퓨터가 되었다고 생각하고 이 코드를 한 줄씩 따라가 보자. food라는 이름의 빈 상자가 하나 있다고 상상해 봐.”

루나는 솔라의 코드 옆에 작은 메모장을 열었다. 그리고 그 위에 네모난 상자를 하나 그렸다.

[ food: "" ]

“자, 프로그램이 시작됐어. menu는 1이야. switch (menu)를 만났으니 어디로 갈까?”

case 1:로 가겠지.”

“맞아. case 1:에 도착했어. food = "아메리카노";를 실행하면, 상자 안에는 뭐가 담기지?”

솔라는 루나의 의도를 따라가며 대답했다. “아메리카노.” 루나는 메모장의 상자 안을 수정했다.

[ food: “아메리카노” ]

“좋아. 그 다음은?”

“음… case 1:이 끝났으니까 switch 문을 빠져나와서 System.out.println을 실행해야지.”

솔라의 대답에 루나는 고개를 저었다.

“정말 그럴까? 코드 어디에도 ‘여기서 멈추고 밖으로 나가라’는 명령이 없는데.”

“명령?”

“응. switch 문은 놀이공원의 자유이용권 같아. case 1:은 1번 놀이기구 입구로 들어가는 티켓이야. 일단 입장하고 나면, 다음 놀이기구로 가는 길을 막는 장애물이 없는 한 계속 앞으로 걸어갈 수 있어.”

루나의 비유에 솔라의 눈이 동그래졌다. ‘장애물이 없는 한 계속 앞으로…’ 솔라는 자신의 코드를 다시 쳐다봤다. case 1: 다음에는 곧바로 case 2:가 있었다. 그 사이를 막는 것은 아무것도 없었다.

“설마… case 1:을 실행하고, 그대로 case 2:로 간다고?”

“한번 따라가 볼까? case 2:에 도착했어. food = "라떼";를 실행하면 상자는 어떻게 될까?”

순간 솔라의 머릿속에 번개가 쳤다.

“아! 덮어쓰는구나! 상자 안에 있던 ‘아메리카노’는 사라지고 ‘라떼’가 담기는 거야.”

루나가 미소 지으며 메모장을 수정했다.

[ food: “라떼” ]

“그 다음은 case 3:. 상자는 ‘카푸치노’가 되고, 마지막으로 default:를 만나서 ‘없는 메뉴’가 되는 거구나! 그래서 무슨 숫자를 넣든 마지막 default 값만 나왔던 거네.”

이제야 모든 것이 이해되었다. switch는 일치하는 case를 찾아주는 점프 기능만 할 뿐, 그곳에서 멈추게 하는 기능은 없었다. 한번 흐름이 시작되면, 누군가 멈추라고 할 때까지 아래로, 아래로 계속 흘러내리는 폭포수와 같았다. 프로그래밍에서는 이 현상을 ‘fall-through’라고 부른다.

“그럼 이 폭포를 멈추게 하는 장애물은 뭔데? 그게 바로 break야?”

“정답. 각 case의 끝에 break;라는 표지판을 세워두면, 코드는 그 case의 작업만 마치고 switch라는 놀이공원 전체를 빠져나가지.”

솔라는 즉시 코드를 수정하기 시작했다. 각 case 블록의 마지막 줄에 break;를 꼼꼼하게 추가했다.

int menu = 1;
String food = "";

switch (menu) {
    case 1:
        food = "아메리카노";
        break;
    case 2:
        food = "라떼";
        break;
    case 3:
        food = "카푸치노";
        break;
    default:
        food = "없는 메뉴";
        break; // default에도 추가하는 것이 안전하다.
}
System.out.println(food + "를 선택하셨습니다.");

수정한 코드를 다시 실행하자, 드디어 솔라가 원했던 결과가 화면에 나타났다.

아메리카노를 선택하셨습니다.

“됐다! 이제야 제대로 동작하네.”

솔라는 break라는 키워드 하나가 코드의 흐름을 얼마나 극적으로 바꾸는지 온몸으로 깨달았다. break는 단순히 case 하나를 끝내는 장치가 아니었다. 그것은 의도치 않은 연쇄 반응을 막고, switch 문 전체의 논리를 지키는 필수적인 안전장치였다.

“그렇구나. switch를 쓸 때는 break를 짝꿍처럼 항상 데리고 다녀야 안전한 거였어.”

fall-through라는 함정과 break라는 안전장치의 역할을 이해한 솔라. 이제 그녀는 switch 문을 이용해 명확하고 안전한 코드를 작성할 준비가 된 듯했다. 이 새로운 도구를 가지고 진짜 메뉴 선택 프로그램을 만들어 볼 차례였다.

3장: switch-case와 break로 만드는 메뉴 선택

솔라의 노트북 화면이 다시 밝아졌다. 콘솔 창에는 아메리카노를 선택하셨습니다. 라는, 방금 전까지 그토록 보고 싶었던 문장이 선명하게 찍혀 있었다. break라는 안전장치 덕분에 switch 문의 폭포수는 알맞은 곳에서 멈춰 섰다. 이제 솔라는 switch 문의 함정과 그 해결법을 알았다.

자신감을 얻은 솔라는 주석으로만 남겨두었던 다음 목표를 향해 시선을 옮겼다.

// 다음 과제: 월(month)을 입력하면 계절을 알려주는 프로그램을 만들어보자.
// 3, 4, 5월 -> "봄"
// 6, 7, 8월 -> "여름"
// 9, 10, 11월 -> "가을"
// 12, 1, 2월 -> "겨울"

“좋아. 이것도 switch로 만들 수 있겠어.”

솔라는 새로운 변수 month를 선언하고 switch 문의 괄호 안에 넣었다. 망설일 필요도 없었다. ‘단일 값 분기’ 상황이라는 걸 이제는 한눈에 알 수 있었다. 그리고 곧장 코드를 써 내려가기 시작했다. fall-through라는 함정을 피하기 위해, 모든 case의 끝에는 반드시 break를 넣어야 한다는 규칙을 철저히 지키면서.

int month = 4;
String season = "";

switch (month) {
    case 1:
        season = "겨울";
        break;
    case 2:
        season = "겨울";
        break;
    case 3:
        season = "봄";
        break;
    case 4:
        season = "봄";
        break;
    // ...
}

한참 코드를 작성하던 솔라의 손가락이 점점 느려졌다. case 5: 까지 쓰고 나자, 이건 뭔가 아니라는 생각이 들었다. 코드가 너무 길고, 무엇보다 같은 내용이 계속 반복됐다. season = "봄"; break; 이라는 똑같은 코드를 세 번이나 써야 했다. 12월까지 다 쓰려면 열두 개의 case와 열두 개의 break가 필요했다.

“이게 최선인가? 차라리 if (month == 3 || month == 4 || month == 5) 라고 쓰는 게 훨씬 짧고 깔끔한 거 아니야?”

솔라는 혼란스러워졌다. switch가 더 깔끔하고 의도를 잘 보여준다고 생각했는데, 이번 경우에는 오히려 if문보다 번거롭게 느껴졌다. break를 꼬박꼬박 챙겨 넣는 것이 안전한 길이라고는 배웠지만, 그 길이 너무나 비효율적으로 보였다.

그때, 소파에 기대앉아 있던 루나가 솔라의 중얼거림을 들었는지 나지막이 물었다.

“네가 막으려고 애썼던 그 현상, 혹시 일부러 이용할 수는 없을까?”

“일부러? fall-through 말이야? 그건 버그 같은 거 아니었어?”

솔라는 의아했다. 의도치 않게 코드가 줄줄 흘러내려 엉뚱한 결과를 낳던 그 끔찍한 기억이 아직 생생했다.

루나는 말없이 솔라의 노트북 화면을 가리켰다. case 3:case 4:, 그리고 아직 쓰지 않은 case 5:가 들어갈 자리가 보였다.

“3월, 4월, 5월은 결국 모두 ‘봄’으로 가야 하잖아. 각 역마다 멈춰서 ‘여기는 봄입니다’라고 외칠 필요가 있을까? 그냥 3번 역에서 기차를 타고, 4번, 5번 역은 정차 없이 통과한 다음, 마지막 ‘봄’ 종착역에 내려서 한 번만 외치면 되지 않을까?”

루나의 비유를 듣는 순간, 솔라의 머릿속에 break가 없는 코드가 다시 떠올랐다. case 1:에서 시작해서 default:까지 멈추지 않고 내달리던 그 폭포수.

‘멈추지 않고 통과한다… 그럼 break를 안 쓰면 되겠네!’

솔라는 무언가 깨달은 듯 코드를 수정하기 시작했다. 먼저 case 3:case 4:에 있던 내용물을 전부 지웠다. 그리고 case 5:에만 season = "봄";break;를 남겨두었다.

// 솔라가 수정한 코드
switch (month) {
    case 3:
    case 4:
    case 5:
        season = "봄";
        break;
    case 6:
    case 7:
    case 8:
        season = "여름";
        break;
    // ... 이하 생략
}

case 3:을 만나면, 실행할 코드가 없으니 아래로 흘러간다. case 4:도 마찬가지. 마침내 case 5:에 도착해서야 season 변수에 “봄”을 할당하고, break를 만나 switch 문 전체를 탈출한다. 3월, 4월, 5월이 모두 같은 목적지를 향해 흘러가는 완벽한 흐름이었다.

코드를 실행하자 결과는 성공이었다. month가 4일 때, 정확히 “봄”이 출력되었다. 길고 반복적이었던 코드는 놀랍도록 간결해졌다.

“와… fall-through가 항상 나쁜 건 아니었구나. 이렇게 일부러 사용하면 코드를 엄청 줄일 수 있네.”

솔라는 감탄했다. break는 의도치 않은 흐름을 막는 ‘안전장치’이기도 했지만, 동시에 흐름을 의도적으로 조절할 수 있게 해주는 ‘밸브’이기도 했다. 무조건 닫아야 하는 것이 아니라, 필요에 따라 열어둘 수도 있는 도구였던 것이다.

이제 switchbreak의 관계를 완전히 이해한 솔라는, 맨 처음 만들려 했던 카페 메뉴 프로그램으로 돌아갔다. 이제는 그저 메뉴를 나열하는 것을 넘어, 조금 더 똑똑한 프로그램을 만들 수 있을 것 같았다.

System.out.println("1. 아메리카노 | 2. 카페라떼 | 3. 카푸치노 | 4. 오늘의 할인 커피");
int choice = 4;
String order = "";

switch (choice) {
    case 4:
        order += "[할인] ";
        // break 없이 fall-through!
    case 1:
        order += "아메리카노";
        break;
    case 2:
        order += "카페라떼";
        break;
    case 3:
        order += "카푸치노";
        break;
    default:
        order = "없는 메뉴";
        break;
}

System.out.println(order + "를 주문하셨습니다.");

솔라는 ‘오늘의 할인 커피’가 ‘아메리카노’인 상황을 fall-through를 이용해 멋지게 구현했다. 4번을 선택하면 order 변수에 [할인] 이라는 문구를 먼저 붙이고, break가 없으므로 그대로 case 1:로 흘러가 ‘아메리카노’라는 이름이 더해진다. 최종 결과는 [할인] 아메리카노.

자신이 작성한 명확하고 안전하면서도, 필요할 땐 유연하게 흐름을 제어하는 코드를 보며 솔라는 만족스럽게 미소 지었다. if문과 switch문. 이제 솔라는 두 가지 갈림길 앞에서 어떤 길을 선택해야 할지, 그리고 그 길을 어떻게 안전하게 건너야 할지 분명히 알게 된 것 같았다.