[C언어 50강] 10강. 조건문 if/else: 중첩, 조건 설계 패턴

[C언어 50강] 10강. 조건문 if/else: 중첩, 조건 설계 패턴

조건문은 “코드를 갈라서 실행한다”는 문법 설명으로 끝내기엔 너무 중요한 주제입니다. 실제 프로그램에서는 입력 검증, 권한 체크, 상태 전이, 예외 처리 대부분이 조건문 품질에 의해 결정됩니다. 오늘은 if/else를 단순 문법이 아니라 조건을 설계하는 도구로 다루겠습니다. 특히 중첩이 왜 복잡해지는지, 어떻게 읽기 쉬운 분기 구조로 바꾸는지에 집중합니다.


핵심 개념

  • if/else의 본질은 참/거짓 자체가 아니라 의도를 코드로 명확히 표현하는 것이다.
  • 중첩 if가 나쁘다는 뜻이 아니라, 조건의 책임이 섞이면 버그 확률이 급격히 올라간다는 뜻이다.
  • 실무에서는 조건을 “판단식”보다 “정책”으로 본다. 즉 우선순위, 경계값, 실패 흐름을 먼저 정의하고 코드화한다.

개념 먼저 이해하기

초보 시절에는 if (조건) { ... } else { ... }만 맞게 쓰면 된다고 생각하기 쉽습니다. 하지만 실제 문제는 조건식 자체보다 조건 간 관계에서 발생합니다. 예를 들어 “점수에 따라 등급을 나눈다”는 요구사항 하나만 봐도, 90점 이상은 A, 80점 이상은 B처럼 우선순위가 명확해야 하고, 경계값(90, 80, 0, 100 초과) 처리 방침이 먼저 정해져야 합니다. 이걸 정하지 않고 코드부터 쓰면 같은 문제를 두고도 개발자마다 분기 순서가 달라지고, 일부 입력에서 엉뚱한 결과가 나옵니다.

또 중요한 점은 C에서 조건식은 단순 불린 타입만 받는 게 아니라 “0이 아니면 참” 규칙을 따른다는 것입니다. 그래서 if (x)x != 0과 같은 의미로 해석됩니다. 이 특성 자체는 강력하지만, 의도를 흐릴 수도 있습니다. 예컨대 if (status)는 status가 1일 때만 참인지, 음수도 참으로 허용하는지 문맥 없이 알기 어렵습니다. 유지보수 관점에서는 if (status == STATUS_OK)처럼 명시적인 표현이 더 안전한 경우가 많습니다.

중첩 if가 어려운 이유는 들여쓰기 때문이 아니라 인지 부하 때문입니다. 사람이 코드를 읽을 때는 “현재 어떤 조건이 이미 보장됐는지”를 머릿속에 계속 들고 있어야 합니다. 중첩이 깊어질수록 이 상태 추적 비용이 커지고, 한 줄 수정이 다른 분기에 미치는 영향을 놓치기 쉽습니다. 그래서 실무에서 자주 쓰는 방식이 ‘가드 절(guard clause)’입니다. 유효하지 않은 케이스를 초반에 빠르게 return하거나 에러 처리로 탈출하면, 핵심 로직이 평평해져 읽기 쉬워집니다.

조건 설계에서 가장 자주 터지는 버그는 세 가지입니다. 첫째, 경계값 누락(>= vs >). 둘째, 순서 실수(큰 범위를 먼저 검사해 작은 범위가 영원히 실행되지 않음). 셋째, 부정 조건 남발(if (!(!a || !b) && !(c == 0)))로 가독성 붕괴. 해결책은 의외로 단순합니다. 조건식을 먼저 종이에 표 형태로 정리하고, 가능한 한 긍정형으로 쓰고, 필요하면 중간 불린 변수를 둬서 의미를 이름으로 드러내는 것입니다.

정리하면 좋은 조건문은 “컴파일되는 코드”가 아니라 “팀원이 읽고 의도를 즉시 이해하는 코드”입니다. 문법은 시작점이고, 진짜 실력은 요구사항을 경계값/우선순위/예외 흐름으로 분해해 안정적인 분기로 구현하는 데서 드러납니다.

기본 사용

예제 1) 점수 등급 분기: 경계값과 순서

#include <stdio.h>

int main(void) {
    int score;
    printf("점수 입력(0~100): ");
    if (scanf("%d", &score) != 1) {
        printf("숫자 입력이 아닙니다.\n");
        return 1;
    }

    if (score < 0 || score > 100) {
        printf("유효 범위를 벗어났습니다.\n");
    } else if (score >= 90) {
        printf("A\n");
    } else if (score >= 80) {
        printf("B\n");
    } else if (score >= 70) {
        printf("C\n");
    } else {
        printf("D\n");
    }

    return 0;
}

설명:

  • 범위 검증을 먼저 하면 비정상 데이터가 등급 로직으로 섞이지 않습니다.
  • >= 90부터 내려오는 순서는 “상위 범주 우선” 정책을 코드로 표현한 것입니다.
  • 동일한 정보를 여러 번 비교하는 대신, 우선순위가 드러나는 체인으로 유지보수성을 확보합니다.

예제 2) 중첩 if를 가드 절로 평탄화

#include <stdio.h>

int process_payment(int is_logged_in, int has_card, int card_valid) {
    if (!is_logged_in) {
        return -1; // 로그인 필요
    }
    if (!has_card) {
        return -2; // 결제수단 없음
    }
    if (!card_valid) {
        return -3; // 카드 만료/정지
    }

    // 핵심 로직은 마지막에 단순하게 남는다.
    return 0;
}

int main(void) {
    int rc = process_payment(1, 1, 0);
    if (rc == 0) {
        printf("결제 성공\n");
    } else {
        printf("결제 실패 코드: %d\n", rc);
    }
    return 0;
}

설명:

  • 중첩 구조(if A { if B { if C { ... }}})를 early return으로 바꿔 가독성을 높였습니다.
  • 실패 원인을 코드로 분리하면 로그/알림 정책을 세우기 쉬워집니다.
  • 실무에서 API 핸들러, 입력 검증, 권한 체크에 매우 자주 쓰는 형태입니다.

예제 3) 의미 있는 불린 변수로 조건 의도 드러내기

#include <stdio.h>

int main(void) {
    int age = 17;
    int has_parent_consent = 1;
    int is_member = 0;

    int is_adult = (age >= 19);
    int can_enter_with_guardian = (age >= 15 && has_parent_consent);
    int can_access = is_member && (is_adult || can_enter_with_guardian);

    if (can_access) {
        printf("입장 가능\n");
    } else {
        printf("입장 불가\n");
    }

    return 0;
}

설명:

  • 긴 조건식을 한 줄에 넣기보다 의미 단위로 분리하면 리뷰와 디버깅이 쉬워집니다.
  • 정책이 바뀔 때(성인 기준 20세) 변수 정의만 수정하면 영향 범위를 파악하기 좋습니다.
  • 조건문의 성능보다 “오해 없이 읽히는가”가 실무 품질에서 더 중요할 때가 많습니다.

예제 4) 중첩 분기에서 상태 우선순위 고정하기

#include <stdio.h>

int main(void) {
    int network_ok = 1;
    int token_ok = 0;
    int maintenance_mode = 0;

    if (maintenance_mode) {
        printf("서비스 점검 중\n");
    } else if (!network_ok) {
        printf("네트워크 오류\n");
    } else if (!token_ok) {
        printf("인증 만료\n");
    } else {
        printf("정상 처리\n");
    }

    return 0;
}

설명:

  • 같은 시점에 여러 문제가 동시에 참일 수 있으므로, 어떤 메시지를 먼저 보여줄지 정책이 필요합니다.
  • 분기 순서는 단순 구현이 아니라 사용자 경험/운영 정책과 연결됩니다.
  • 이 우선순위를 주석/문서와 함께 유지하면 장애 대응 시 혼선을 줄일 수 있습니다.

자주 하는 실수

실수 1) 범위 검사 순서를 잘못 두어 분기가 가려짐

  • 원인: if (score >= 60) ... else if (score >= 90)처럼 넓은 범위를 먼저 검사함.
  • 해결: 더 구체적이고 우선순위 높은 조건을 먼저 배치하고, 테스트 케이스에 경계값(59, 60, 89, 90)을 반드시 포함합니다.

실수 2) ===를 혼동

  • 원인: 조건식에서 대입 연산을 써도 C 문법상 허용되어 컴파일이 됨.
  • 해결: 경고 옵션(-Wall -Wextra)을 켜고, 비교는 상수/열거형 중심으로 명시적으로 작성합니다.

실수 3) 부정 조건을 과도하게 중첩

  • 원인: 빠르게 붙여 쓰다 보니 !가 늘어나 의도가 안 보임.
  • 해결: 긍정형 이름의 중간 변수(is_valid_input)를 만들고, 필요하면 드모르간 변환으로 식을 단순화합니다.

실수 4) else에 모든 예외를 몰아넣음

  • 원인: “나머지 케이스”를 충분히 정의하지 않고 기본 처리로 퉁침.
  • 해결: 예상 가능한 실패 유형을 먼저 분리하고, 진짜 미정의 케이스만 마지막 else에서 로깅/방어 처리합니다.

실무 패턴

  • 조건문 작성 전, 요구사항을 결정 테이블(입력 조건 vs 결과)로 먼저 정리합니다.
  • 경계값 테스트를 코드 리뷰 체크리스트에 포함합니다: 최소/최대/직전/직후 값.
  • 깊은 중첩이 보이면 “가드 절 + 함수 분리”를 우선 검토합니다.
  • 복잡 조건은 이름 있는 불린 변수로 분해해 문서 역할을 하게 만듭니다.
  • 오류 메시지 우선순위를 제품 정책과 맞춥니다(사용자에게 무엇을 먼저 알려줄지).

오늘의 결론

한 줄 요약: 좋은 if/else는 문법이 아니라 **정책(우선순위·경계값·실패 흐름)**을 명확히 담아낸 설계다.

연습문제

  1. 나이와 회원 여부를 입력받아 입장 가능/불가를 판단하세요. 조건: 회원이 아니면 무조건 불가, 회원이면 19세 이상 또는 보호자 동의 시 가능.
  2. 점수 등급 분기 코드를 작성하되, 0 미만/100 초과는 “INVALID”를 출력하고 종료하세요. 경계값 테스트 케이스를 6개 이상 적어보세요.
  3. 중첩 if 3단계로 작성된 코드를 가드 절 패턴으로 리팩터링하고, 가독성이 좋아진 이유를 3문장으로 설명하세요.

이전 강의 정답

지난 9강(표준 입출력) 연습문제 해설:

  1. 정수 2개 합 계산 + 재입력 루프
  • 핵심 정답: scanf 반환값을 매번 확인하고, 실패 시 getchar()로 줄 끝까지 버퍼를 비운 뒤 다시 입력받아야 합니다.
  • 포인트: 실패 입력을 정리하지 않으면 같은 토큰이 남아 무한 실패가 반복됩니다.
  1. %c 개행 문제 재현/수정
  • 재현: scanf("%d", &n); scanf("%c", &ch);처럼 쓰면 ch\n이 들어올 수 있습니다.
  • 수정: scanf(" %c", &ch);로 공백 스킵을 명시하면 해결됩니다.
  • 이유: %c는 공백을 자동 무시하지 않지만, 포맷 문자열의 선행 공백은 공백류 문자를 소비합니다.
  1. fgets + strtol 나이 검증 루프
  • 핵심 구조: fgets로 한 줄을 안전하게 받고 strtol로 변환한 뒤, 변환 실패/범위 실패를 구분하여 재입력.
  • 유효 조건 예시: 0~120.
  • 포인트: 입력 수집과 의미 해석을 분리하면 에러 처리가 훨씬 견고해집니다.

실습 환경/재현 정보

  • 컴파일러: Apple clang 17.x 또는 GCC 13+
  • 컴파일 옵션: -std=c11 -Wall -Wextra -Wconversion -O0
  • 실행 환경: macOS (arm64), 터미널 zsh
  • 재현 체크:
    • clang -std=c11 -Wall -Wextra -Wconversion -O0 lesson10.c -o lesson10
    • ./lesson10
    • 경계값 입력(예: -1, 0, 19, 20, 100, 101)을 넣어 분기 결과를 확인합니다.