[C언어 50강] 11강. switch-case: 분기 처리, fall-through 주의점
C언어는 문법을 외우는 과목이 아니라, 조건을 어떤 방식으로 분해하고 실행 흐름을 통제할지 설계하는 훈련에 가깝습니다. 이번 강의에서는 switch-case를 단순히 "if/else의 대체 문법"으로 보지 않고, 분기 전략을 명확하게 표현하는 도구로 이해해보겠습니다. 특히 C에서 자주 사고가 나는 fall-through(break 누락으로 다음 case가 연쇄 실행되는 현상)를 중심으로, 왜 문제가 생기고 어떻게 예방하는지까지 연결해서 정리합니다.
핵심 개념
switch는 "하나의 기준값"을 여러 정수 계열 상수와 비교해 분기하는 구조다.case블록은 기본적으로 자동 종료되지 않으며,break로 의도를 명시해야 한다.fall-through는 버그가 될 수도, 의도된 동작이 될 수도 있으므로 코드에 의도를 드러내야 한다.
개념 먼저 이해하기
switch-case를 처음 배울 때 많은 분이 "if/else를 짧게 쓰는 문법" 정도로 기억합니다. 틀린 말은 아니지만, 이 수준에서 멈추면 실무에서 금방 사고가 납니다. 핵심은 switch는 조건식이 아니라 분기 테이블에 가까운 구조라는 점입니다. if/else if는 각 조건이 독립된 논리식으로 평가됩니다. 반면 switch는 먼저 기준값 하나를 구한 뒤, 그 값과 일치하는 case로 점프합니다. 즉, switch는 "여러 복잡한 조건을 계산"하는 구조가 아니라, "하나의 상태값(state)·코드값(code)·메뉴번호(menu id)를 해석"하는 구조에 훨씬 잘 맞습니다.
여기서 첫 번째 오해가 생깁니다. "case 하나 실행하고 자동으로 끝나겠지"라는 기대입니다. C의 switch는 그렇지 않습니다. 매칭된 case 라벨 위치로 이동한 뒤, break를 만날 때까지 아래 코드를 계속 실행합니다. 이 동작을 fall-through라고 부릅니다. 초보 단계에서는 거의 90%가 버그 원인이지만, 고급 코드에서는 의도적으로 쓰기도 합니다(예: 여러 case에서 같은 처리를 공유). 문제는 의도와 결과가 코드에서 구분되지 않으면, 읽는 사람이 전부 버그로 의심하게 된다는 점입니다.
두 번째 오해는 switch가 모든 타입에 다 된다고 생각하는 것입니다. C에서 switch의 제어식은 정수 계열(정수형, 문자형, enum 등)이어야 합니다. 문자열 자체를 switch에 바로 넣을 수 없습니다. 그래서 메뉴 명령어를 문자열로 입력받았다면, 먼저 숫자 코드나 enum으로 변환한 뒤 switch를 적용하는 패턴이 안정적입니다.
세 번째 오해는 default를 "옵션"으로 보는 태도입니다. 학습용 예제에서는 생략해도 돌아가지만, 실무에서는 예상 밖 값이 반드시 들어옵니다. 네트워크 패킷 오류, 파일 손상, 사용자 오입력, API 스펙 변경 등 현실은 늘 불완전합니다. default는 단순 예외 처리 블록이 아니라, 시스템이 모르는 상태를 만났을 때 안전하게 실패하는 마지막 방어선입니다.
성능 관점에서도 switch를 이해할 필요가 있습니다. 컴파일러는 case 값의 분포에 따라 점프 테이블(jump table)이나 이진 탐색 형태로 최적화할 수 있습니다. 그래서 연속된 정수 case가 많은 구조에서는 if/else 연쇄보다 유리한 경우가 자주 있습니다. 하지만 여기서 중요한 건 "switch가 항상 빠르다"가 아니라, 분기 의도가 명확할수록 컴파일러가 최적화하기 쉬워진다는 사실입니다. 즉, 좋은 분기 설계는 가독성과 성능을 동시에 챙기는 출발점입니다.
정리하면, switch는 문법 문제가 아니라 설계 문제입니다. "이 값의 상태에 따라 어떤 동작을 할지"를 읽기 좋은 형태로 고정하는 도구이며, break/default/의도적 fall-through 주석까지 포함해 미래의 나와 동료가 오해하지 않게 만드는 것이 핵심입니다.
기본 사용
예제 1) 최소 동작 예제
#include <stdio.h>
int main(void) {
int menu = 2;
switch (menu) {
case 1:
printf("조회 기능 실행\n");
break;
case 2:
printf("등록 기능 실행\n");
break;
case 3:
printf("삭제 기능 실행\n");
break;
default:
printf("알 수 없는 메뉴 번호: %d\n", menu);
break;
}
return 0;
}
설명:
menu라는 단일 기준값으로 분기합니다. switch가 잘 맞는 전형적인 형태입니다.- 각
case의 끝에break를 넣어 "한 동작만 실행"되도록 고정합니다. default는 예상 범위를 벗어난 입력을 처리합니다.- 흐름을 제어하는 문법(
break)이 빠지면 의미가 바뀌므로, 로직보다 제어문이 더 중요할 때가 많습니다.
예제 2) 실무에서 자주 맞닥뜨리는 패턴
#include <stdio.h>
enum LogLevel {
LOG_DEBUG = 10,
LOG_INFO = 20,
LOG_WARN = 30,
LOG_ERROR = 40
};
void print_log_prefix(enum LogLevel level) {
switch (level) {
case LOG_DEBUG:
printf("[DEBUG] ");
break;
case LOG_INFO:
printf("[INFO ] ");
break;
case LOG_WARN:
printf("[WARN ] ");
break;
case LOG_ERROR:
printf("[ERROR] ");
break;
default:
printf("[UNKWN] ");
break;
}
}
int main(void) {
enum LogLevel level = LOG_WARN;
print_log_prefix(level);
printf("디스크 사용량이 90%%를 초과했습니다.\n");
return 0;
}
설명:
- 숫자 매직넘버 대신
enum을 써서 상태값의 의미를 코드에 드러냅니다. switch는 enum 해석기 역할을 하며, 규칙이 늘어나도 구조가 쉽게 유지됩니다.default를 넣어 미정의 값이 들어와도 포맷이 깨지지 않게 합니다.- 이런 "코드값 → 행동" 매핑은 설정 처리, 프로토콜 해석, 상태머신 등에 반복적으로 등장합니다.
예제 3) 디버깅 포인트 포함 예제
#include <stdio.h>
int main(void) {
int score = 85;
switch (score / 10) {
case 10:
case 9:
printf("A 등급\n");
break;
case 8:
printf("B 등급\n");
/* 의도적 fall-through: B와 C는 공통 피드백 출력 */
case 7:
printf("기본 개념 복습 권장\n");
break;
case 6:
printf("D 등급\n");
break;
default:
printf("F 등급\n");
break;
}
return 0;
}
설명:
case 10과case 9를 묶는 것은 의도된 fall-through의 좋은 예입니다.case 8에서break를 일부러 생략해case 7의 공통 안내문을 재사용했습니다.- 이때 반드시 주석으로 의도를 남겨야 리뷰/유지보수에서 오해를 줄일 수 있습니다.
- 디버깅할 때는 "원치 않는 fall-through인지, 의도된 공유 실행인지"를 먼저 구분해야 문제를 빨리 찾습니다.
자주 하는 실수
실수 1) break 누락으로 여러 case가 연쇄 실행됨
- 원인: case 하나가 끝나면 자동 종료된다고 착각함.
- 해결: 기본 원칙은 "모든 case 끝에 break"입니다. 예외적으로 공유 실행이 필요하면
/* 의도적 fall-through */주석을 명시하세요.
실수 2) default를 생략해 예외 입력이 조용히 무시됨
- 원인: 정상 입력만 온다고 가정함.
- 해결: default에서 최소한 로그/에러 메시지를 남기고, 필요 시 오류 코드를 반환해 호출자가 대응하게 설계합니다.
실수 3) switch로 처리하기 어려운 복합 조건까지 억지로 넣음
- 원인: "분기문은 switch가 더 깔끔하다"는 고정관념.
- 해결: 범위 비교(
x > 10 && x < 20)처럼 관계식 중심 로직은 if/else가 더 명확합니다. switch는 코드값 해석에 집중하세요.
실무 패턴
- 상태/코드 중심 설계: 메뉴 번호, 이벤트 타입, 파서 토큰처럼 "정수 코드"를 먼저 정규화하고 switch에서 해석합니다.
- default는 필수: 알 수 없는 값 처리 정책(로그, 무시, 종료)을 팀 규칙으로 고정합니다.
- 의도적 fall-through 문서화: 주석 없이는 금지하는 팀도 많습니다. 정적 분석기 경고 정책과 함께 운영하면 효과적입니다.
- case 블록 길이 제한: case가 길어지면 함수로 분리(
handle_menu_create()등)해 switch는 라우팅 역할만 하게 만듭니다. - 입력 검증 선행: switch 전에 값 범위를 검증하면 default는 진짜 예외 처리에 집중할 수 있습니다.
오늘의 결론
한 줄 요약: switch-case의 본질은 "분기 문법"이 아니라 "코드값 해석기"이며, break와 default로 의도를 명확히 해야 안전한 프로그램이 된다.
연습문제
- 1~7 사이 요일 번호를 입력받아 요일명을 출력하는 프로그램을
switch로 작성하세요. 잘못된 번호는default에서 처리하세요. score/10방식으로 학점을 출력하되, 100점(A+)를 별도로 처리하도록case 10을 설계해보세요.- 의도적 fall-through를 1회 사용하는 예제를 만들고, 주석 없이 봤을 때 왜 위험한지 설명해보세요.
이전 강의 정답
지난 10강(조건문 if/else) 연습문제 기준 예시 정답 요약입니다.
- 문제 1) 세 수 중 최댓값 찾기: 비교 순서를 고정해
max를 갱신하는 패턴이 가장 읽기 쉽습니다. - 문제 2) 점수 구간 판정: 상한부터 검사(
>= 90,>= 80...)하면 중복 조건을 줄일 수 있습니다. - 문제 3) 중첩 if 리팩터링: 조기 반환(early return) 또는 else-if 체인으로 평탄화하면 버그가 줄어듭니다.
간단 예시:
#include <stdio.h>
int max3(int a, int b, int c) {
int max = a;
if (b > max) max = b;
if (c > max) max = c;
return max;
}
int main(void) {
printf("max=%d\n", max3(7, 2, 9));
return 0;
}
실습 환경/재현 정보
- 컴파일러: Apple clang 17.x 또는 gcc 13+
- 컴파일 옵션:
-Wall -Wextra -Werror -std=c11 - 실행 환경: macOS(arm64) 터미널 / Linux x86_64 터미널
- 재현 체크:
break를 일부러 지우고 출력이 어떻게 바뀌는지 확인default제거 후 이상 입력에서 어떤 문제가 생기는지 확인enum값 하나를 비정상 숫자로 바꿔 default 경로가 동작하는지 확인