15장: 조건문 사용

15장: 조건문 사용

15.1 if 문

if 문의 종류는 복잡도 순으로 세 가지

  • 단순 if (if-then) — 가장 단순
  • if-then-else — 조금 더 복잡
  • if-then-else-if 연쇄(chain) — 가장 복잡

단순 if-then 문

지침 1 - 정상 경로를 먼저 쓰고, 특이 사례는 나중에 써라

Write the nominal path through the code first; then write the unusual cases

  • 코드의 정상 경로(normal path)가 분명히 보이게 작성
    • 일반적인 경우 우선
지침 2 - 등호 분기가 올바른지 확인하라

Make sure that you branch correctly on equality

  • >= 대신 >를 쓰는 것은 배열 접근이나 루프 인덱스의 off-by-one 오류와 같은 부류
  • 루프에서 끝점(endpoint)을 따져보듯, 조건문에서는 등호 케이스를 따져볼 것
지침 3 - 정상 케이스를 else가 아니라 if 뒤에 둬라

Put the normal case after the if rather than after the else

  • 평소에 처리할 것으로 기대하는 케이스를 먼저 둠 (위에 두기)
  • “결정에서 나온 코드는 결정과 최대한 가까이 둔다”는 일반 원칙과 같은 맥락
  • 책 예제: 파일 열기 → 읽기 → 요약 → 저장을 하며 오류를 처리하는 코드
    • 나쁜 버전: 오류 케이스가 어떤 곳은 if 절에, 어떤 곳은 else 절에 뒤죽박죽 → 정상 경로가 어디인지, 정상 케이스가 어느 if에 걸리는지 찾기 어려움
    • 좋은 버전: 정상 경로를 일관되게 먼저, 오류 케이스는 전부 마지막에 → if 검사의 본류만 읽으면 정상 케이스가 보임

The stack of error conditions at the bottom of the nest is a sign of well-written error-processing code.

  • 바닥에 중첩해서 오류 조건들이 쌓여 있는 모양이 잘 작성된 오류 처리 코드의 모양
  • 이 방식 말고도 해법이 여럿 있음:
    • guard clause
    • 다형성(polymorphic dispatch) 전환
    • 내부 테스트를 별도 루틴으로 추출
      • 깊은 중첩 구조를 해결하는 전체 방법은 19.4절 “위험할 정도로 깊은 중첩 길들이기” 참고

대안 1: guard clause (보호 절, early return) — 오류 케이스를 만나는 즉시 반환해서 중첩 자체를 없앰

OpenFile( inputFile, status );
if ( status == Status_Error ) return ErrorType_FileOpenError;

ReadFile( inputFile, fileData, status );
if ( status == Status_Error ) return ErrorType_FileReadError;

SummarizeFileData( fileData, summaryData, status );
if ( status == Status_Error ) return ErrorType_DataSummaryError;

PrintSummary( summaryData );
SaveSummaryData( summaryData, status );
if ( status == Status_Error ) return ErrorType_SummarySaveError;

UpdateAllAccounts();
EraseUndoFile();
return ErrorType_None;
  • 정상 경로가 들여쓰기 없이 위에서 아래로 쭉 읽히고, 각 단계의 오류 처리가 그 단계 바로 옆에 붙음
  • 지침 3의 중첩 버전과 하는 일은 같지만, 오류를 바닥에 쌓는 대신 위에서 하나씩 걸러냄.

대안 2: 다형성 전환 — 조건문이 “형(type) 검사”라면, 조건문을 없애고 각 형이 스스로 행동하게 함

// 전: 형 검사 if-then-else 연쇄
if ( shape.type == ShapeType_Circle ) { DrawCircle( shape ); }
else if ( shape.type == ShapeType_Square ) { DrawSquare( shape ); }
else if ( shape.type == ShapeType_Triangle ) { DrawTriangle( shape ); }

// 후: 각 하위 클래스가 자기 Draw()를 구현
shape.Draw();
  • 분기가 코드에서 사라지고 언어의 디스패치로 이동함. 새 도형이 추가돼도 조건문을 고칠 필요가 없음
  • 단, 파일 처리 예제 같은 “절차의 단계별 오류 검사”에는 부자연스러움 — 형에 따라 갈라지는 분기에 맞는 도구

대안 3: 내부를 별도 루틴으로 추출 — 중첩의 안쪽 덩어리를 루틴으로 빼서, 한 루틴에서 보이는 깊이를 낮춤

OpenFile( inputFile, status );
if ( status == Status_Success ) {
    errorType = ProcessFileContents( inputFile );  // 읽기~저장의 안쪽 중첩 전체를 추출
}
else {
    errorType = ErrorType_FileOpenError;
}
  • 전체 논리는 그대로지만 루틴 하나가 감당하는 복잡도가 줄어듦 — 7장 “루틴을 작성하는 이유”(복잡도 낮추기)와 연결
지침 4 - if 절 뒤에는 의미 있는 문장을 둬라

Follow the if clause with a meaningful statement

  • null if 절 (Coding Horror)
    • if 절이 empty
if ( SomeTest )
    ;
else {
    // do something
}
  • if 조건을 부정하고, else 절 코드를 if 절로 옮기고, else 절을 없애면 됨
if ( ! someTest ) {
    // do something
}
지침 5 - else 절을 고려하라

Consider the else clause

  • 단순 if가 필요하다고 생각되면, 실은 if-then-else가 필요한 것 아닌지 고려

A classic General Motors analysis found that 50 to 80 percent of if statements should have had an else clause (Elshoff 1976).

  • HARD DATA: GM의 고전적 분석 — if 문의 50~80%는 else 절이 있었어야 했음
  • 선택지 1: else 케이스를 고려했음을 보여주기 위해 (필요하면 null 문장으로라도) else 절을 코딩 — 과할 수 있지만 최소한 else 케이스를 고려는 할 것
  • 선택지 2: else가 필요 없는 이유가 자명하지 않다면 주석으로 설명
// if color is valid
if ( COLOR_MIN <= color && color <= COLOR_MAX ) {
    // do something
}
else {
    // else color is invalid
    // screen not written to -- safely ignore command
}
지침 6 - else 절의 정확성을 테스트하라

Test the else clause for correctness

  • if 절만 테스트하면 된다고 생각하기 쉽지만, else 절을 테스트할 수 있다면 반드시 테스트할 것
지침 7 - if와 else 절이 뒤바뀌지 않았는지 확인하라

Check for reversal of the if and else clauses

  • 흔한 실수: if 절 코드와 else 절 코드를 서로 바꿔 넣거나, if 검사의 논리를 거꾸로 씀

if-then-else 연쇄

  • case 문이 없거나 부분적으로만 지원하는 언어에서는 if-then-else 검사의 연쇄를 자주 쓰게 됨
  • 책 예제: 문자를 제어 문자/구두점/숫자/글자로 분류하는 연쇄
지침 1 - 복잡한 검사는 불린 함수 호출로 단순화하라

Simplify complicated tests with boolean function calls

  • 분류 조건이 복잡하면 (구두점 10개를 ||로 나열하는 식) 읽기 어려움
  • 검사를 불린 함수로 대체하면 연쇄가 한눈에 읽힘
if ( IsControl( inputCharacter ) ) {
    characterType = CharacterType_ControlCharacter;
}
else if ( IsPunctuation( inputCharacter ) ) {
    characterType = CharacterType_Punctuation;
}
else if ( IsDigit( inputCharacter ) ) {
    characterType = CharacterType_Digit;
}
else if ( IsLetter( inputCharacter ) ) {
    characterType = CharacterType_Letter;
}
지침 2 - 가장 흔한 케이스를 앞에 둬라

Put the most common cases first

  • 가독성: 흔한 케이스를 찾으려고 예외 처리 코드를 헤집는 양이 줄어듦
  • 효율: 흔한 케이스를 찾는 데 수행되는 검사 횟수가 줄어듦
  • 위 예제에서는 글자가 구두점보다 흔하므로 IsLetter 검사를 맨 앞으로
지침 3 - 모든 케이스가 처리되는지 확인하라

Make sure that all cases are covered

  • 마지막 else 절에 오류 메시지나 어설션을 넣어 계획하지 못한 케이스를 잡을 것
    • pocu 코딩 표준에서도 강조
  • 이 메시지는 사용자용이 아니라 개발자용 — 그에 맞게 작성
else {
    DisplayInternalError( "Unexpected type of character detected." );
}
지침 4 - 언어가 지원한다면 다른 구조로 대체하라

Replace if-then-else chains with other constructs if your language supports them

  • Visual Basic, Ada처럼 문자열·열거형·범위를 지원하는 강력한 case 문이 있는 언어라면 case 문을 쓸 것 — if-then-else 연쇄보다 코딩하기도 읽기도 쉬움
Select Case inputCharacter
    Case "a" To "z"
        characterType = CharacterType_Letter
    Case " ", ",", ".", "!", "(", ")", ":", ";", "?", "-"
        characterType = CharacterType_Punctuation
    Case "0" To "9"
        characterType = CharacterType_Digit
    ...
    Case Else
        DisplayInternalError( "Unexpected type of character detected." )
End Select

15.2 case 문

case(switch) 문은 언어마다 차이가 큰 구조물

  • C++/Java: 서수형(ordinal type)을 한 번에 한 값씩만 지원
  • Visual Basic: 서수형 + 범위와 값 조합을 표현하는 강력한 축약 표기 지원
  • 많은 스크립트 언어: case 문 자체가 없음

케이스의 효과적인 순서 정하기

  • 케이스 3개짜리 작은 case 문이면 순서는 별로 중요하지 않음
  • 긴 case 문(예: 이벤트 기반 프로그램에서 수십 개 이벤트를 처리)이라면 순서가 중요함
순서 1 - 알파벳순 또는 숫자순

Order cases alphabetically or numerically

  • 케이스들이 똑같이 중요하다면 A-B-C 순서가 가독성을 높임 — 특정 케이스를 무리에서 골라내기 쉬움
순서 2 - 정상 케이스를 먼저

Put the normal case first

  • 정상 케이스 하나 + 예외 여럿이라면 정상 케이스를 먼저
  • 어느 것이 정상이고 어느 것이 특이 케이스인지 주석으로 표시
순서 3 - 빈도순

Order cases by frequency

  • 가장 자주 실행되는 케이스를 앞에, 가장 드문 케이스를 뒤에
  • 독자가 찾는 케이스는 대개 흔한 케이스 — 흔한 것을 위에 두면 찾기가 빨라짐

case 문 사용 팁

팁 1 - 각 케이스의 동작은 단순하게 유지하라

Keep the actions of each case simple

  • 각 케이스에 붙는 코드가 짧아야 case 문의 구조가 분명하게 보임
  • 케이스의 동작이 복잡하면 코드를 케이스 안에 넣지 말고 루틴으로 빼서 호출할 것
팁 2 - case 문을 쓰려고 가짜 변수를 만들지 마라

Don’t make up phony variables to be able to use the case statement

  • case 문은 쉽게 분류되는 단순한 데이터를 위한 것. 데이터가 단순하지 않으면 if-then-else 연쇄를 쓸 것
  • 나쁜 예 (Coding Horror): 사용자가 입력한 명령 문자열의 첫 글자만 떼어 switch
action = userCommand[ 0 ];
switch ( action ) {
    case 'c':
        Copy();
        break;
    case 'd':
        DeleteCharacter();
        break;
    ...
    default:
        HandleUserInputError( ErrorType.InvalidUserCommand );
}
  • 문제: copy를 입력해도 Copy()가 불리지만, cement overshoes, clambake, cellulite를 입력해도 첫 글자 ‘c’만 보고 Copy()가 불림
  • default의 오류 검사도 무력화됨 — 잘못된 첫 글자만 거를 뿐 잘못된 명령은 거르지 못함
  • 올바른 재작성: 가짜 변수 대신 if-then-else 연쇄로 문자열 전체를 검사 (userCommand.equals( COMMAND_STRING_COPY ) …)
    • 언어에서 문자열 내용 비교를 제공하면 굿
팁 3 - default 절은 정당한 기본값(legitimate defaults) 탐지에만 사용하라

Use the default clause only to detect legitimate defaults

  • 마지막 남은 케이스 하나를 default 절로 코딩하고 싶은 유혹 — 저자 왈 “멍청한 짓(that’s dumb)”
  • 잃는 것 두 가지
    1. 케이스 레이블이 주는 자동 문서화
    2. default 절의 오류 탐지 능력
  • 수정에도 취약함: 새 케이스를 추가하고 기존 default를 정당한 케이스(legitimate case)로 되돌리는 작업까지 해야 함
팁 4 - default 절로 오류를 탐지하라

Use the default clause to detect errors

  • default 절이 다른 용도로 쓰이지 않고 발생할 일도 없다면 진단 메시지를 넣을 것
default:
    DisplayInternalError( "Internal Error 905: Call customer support." );
  • 이런 메시지는 디버깅과 프로덕션 코드 양쪽에서 유용함
  • 사용자도 시스템 크래시나, 맞아 보이다가 상사가 확인할 때 틀리는 미묘한 오류보다 “고객 지원에 연락하세요” 메시지를 선호함
  • default를 오류 탐지 외 용도로 쓴다면 모든 케이스 선택자가 올바르다는 뜻이 됨
    • 들어올 수 있는 모든 값이 정당한지 재확인하고, 아니라면 default가 오류를 검사할 수 있도록 재작성할 것
  • POCU 코딩 표준
팁 5 - C++/Java에서 case 끝의 fall through를 피하라

In C++ and Java, avoid dropping through the end of a case statement

  • C 계열 언어(C, C++, Java)는 각 케이스에서 자동으로 빠져나가지 않음 — break를 명시하지 않으면 다음 케이스 코드가 이어서 실행됨
  • 최악의 남용 예 (Coding Horror): case 'A':의 if 블록 안에 case 'B':가 들어 있는 코드
    • 제어 구조가 뒤섞임(intermingle) — 중첩된 제어 구조도 어려운데 겹쳐진(overlapping) 제어 구조는 이해가 거의 불가능
    • 이런 케이스의 수정은 “뇌수술보다 어려움”
팁 6 - C++에서 의도적 fall through는 분명하고 확실하게 표시하라

In C++, clearly and unmistakably identify flow-throughs at the end of a case statement

  • 의도적으로 fall through를 쓴다면, 일어나는 지점에 주석을 달고 왜 그렇게 코딩해야 했는지 설명할 것
switch ( errorDocumentationLevel ) {
    case DocumentationLevel_Full:
        DisplayErrorDetails( errorNumber );
        // FALLTHROUGH -- Full documentation also prints summary comments
    case DocumentationLevel_Summary:
        DisplayErrorSummary( errorNumber );
        // FALLTHROUGH -- Summary documentation also prints error number
    case DocumentationLevel_NumberOnly:
        DisplayErrorNumber( errorNumber );
        break;
    default:
        DisplayInternalError( "Internal Error 905: Call customer support." );
}

This technique is useful about as often as you find someone who would rather have a used Pontiac Aztek than a new Corvette.

  • 이 기법이 유용한 빈도 = “새 콜벳보다 중고 폰티악 아즈텍을 갖고 싶어하는 사람을 만날 빈도”
    • 비유가 와닿지는 않음
  • 일반적으로 케이스 간 fall through는 수정 과정의 실수를 부르는 초대장이므로 피할 것
  • POCU 코딩 표준

results matching ""

    No results matching ""