19장 - 제어와 관련된 일반적인 이슈

Edited by / 임수형 (sulogc)

제어 구조를 생각할 때 떠오르는 일반적이고 실용적인 이슈에 대해 다룬다.

19-1 불린 표현식

순차적인 기본 제어 구조를 제외하며, 모든 제어 구조는 불린 표현식의 평가에 의존한다.

불린 테스트에서 true와 false 사용하기.

  • 0, 1과 같은 플래그는 의미에 혼동을 주기 때문에 적절하지 않다.
  • True와 false 상수를 사용하면 의도를 더 분명히 나타낼 수 있다.
  • 암묵적으로 true와 false에 직접 비교하는 것이 좋다.
    while ( done = false ) ...
    while ( (a > b) = true ) ...
    ---
    while ( not done ) ...
    while ( a > b ) ...
    

    복잡한 표현식을 단순하게 만들기

  • 새로운 불린 변수를 통해 부분적으로 나누어라.
  • 복잡한 표현식을 불린 함수로 옮겨라.
Visual Basic Example of a Complicated Test
If ( ( document.AtEndOfStream ) And ( Not inputError ) ) And _
 ( ( MIN_LINES <= lineCount ) And ( lineCount <= MAX_LINES ) ) And _
 ( Not ErrorProcessing( ) ) Then
 ' do something or other

---

Function DocumentIsValid( _
 ...
 allDataRead = ( documentToCheck.AtEndOfStream ) And ( Not inputError )
 legalLineCount = ( MIN_LINES <= lineCount ) And ( lineCount <= MAX_LINES )
 DocumentIsValid = allDataRead And legalLineCount And ( Not ErrorProcessing() )
End Function

 ...
End If
  • 의사결정 테이블을 사용해 복잡한 조건을 대체하라.

불린 표현식을 긍정문으로 작성하기

대부분의 사람들은 긴 부정문을 이해하기 어려워한다.

  • if 문에서는 부정문을 긍정문으로 변환한 다음 if 절과 else 절에 있는 코드를 바꾸어라.
  • 드모르간의 법칙을 적용해 부정 불린 테스트를 단순화하라.
if ( !displayOK || !printerOK ) ...
---
if ( !( displayOK && printerOK ) ) ...

괄호를 사용해 이해하기 쉬운 불린 표현식 만들기

복잡한 불린 표현식이 있다면 언어의 평가 순서에 의존하지 말고, 괄호를 사용해 의미를 분명히 하라.

if ( a < b == c == d ) ...
---
if ( ( a < b ) == ( c == d ) ) ...

위의 경우 괄호가 코드를 이해하고 프로그램의 정확성을 유지하는 데 도움을 준다.

  • 괄호의 균형을 유지하기 위해서 간단한 계산 기법을 사용하라.
if ( ( ( a < b ) == ( c == d ) ) && !done ) ...
   | | |       |    |        | |          |
   0 1 2       3    2        3 2          1 0

일종의 스택…

  • 논리 표현식에는 충분할 정도로 괄호를 사용하라

불린 표현식 평가 방법

단축 표현식 사용 방법.

숫자의 크기 순서대로 수치 표현식 작성하기

수치 테스트는 숫자의 크기 순서를 따르도록 구성한다.

MIN_ELEMENTS <= i and i <= MAX_ELEMENTS
i < MIN_ELEMENTS or MAX_ELEMENTS < i

다음을 쉽게 연상할 수 있다.

0을 비교하는 방법

프로그램에서는 다양한 목적으로 0을 사용한다.

  • 논리적인 변수를 암시적으로 비교하라 논리 표현식은 다음과 같이 작성하는 것이 좋다.
    while ( !done )
    
  • 숫자는 0과 명시적으로 비교하라
    while ( balance ) ...
    ---
    while ( balance != 0 ) ...
    
  • 포인터 NULL과 비교하라
    while ( bufferPtr ) ...
    ---
    while ( bufferPtr != NULL ) ...
    

19-2 복합문(블록)

복합문 또는 블록은 프로그램의 흐름을 제어하기 위해서 단일 명령문으로 취급하는 명령문의 집합을 말한다. 다음은 복합문을 효과적으로 사용하는 방법

  • 중괄호 쌍을 함께 작성하라.
    for ( i = 0; i < maxLines; i++ )
    ---
    for ( i = 0; i < maxLines; i++ ) { }
    ---
    for ( i = 0; i < maxLines; i++ ) {
     // whatever goes in here ...
    }
    

    쉽게 짝을 맞출 수 있다.

  • 중괄호를 사용하여 조건문을 분명히 하라. 조건문은 어떤 명령문이 if 테스트로 실행되는지 알지 못하면 이해하기 어렵다. 볼록 내 코드 길이에 상관없이 블록 구조를 사용하여 의도를 분명히 하라.

19.3 널(Null) 명령문

C++에서는 다음과 같이 세미콜론 만으로 이루어진 널 명령문을 작성할 수 있다.

while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() )
    ;

세미콜론 자체가 널 명령문이다. 다음은 C++에서 널 명령문을 다룰 때 참고할 만한 사항이다.

  • 널 명령문을 눈에 띄게 만들어라. 일반적인 명령문이 아니기 때문에 눈에 띄게 만들어야한다. 다른 명령문처럼 들여쓰기 하거나 빈 중괄호를 사용할 수 있다.

  • 널 명령문을 사용하기 위한 DoNothing() 전처리기 매크로나 인라인 함수를 작성하라
    #define DoNothing()
    ...
    while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() ) {
     DoNothing();
    }
    
  • 널이 아닌 반복문을 사용하는 것이 더 분명하지 않은지 고려하라. 몸체에서 아무것도 수행하지 않는 반복문 코드의 대부분은 반복문 제어 코드에서 작업을 수행한다. 작업을 명시적으로 수행하면 더 읽기 쉬운 코드를 작성할 수 있다.
    RecordType record = recordArray.Read( index );
    index++;
    while ( record != recordArray.EmptyRecord() ) {
     record = recordArray.Read( index );
     index++;
    }
    

19.4 지나치게 깊은 중첩 구조 처리

들여쓰기 깊이가 지나치게 깊은 코드는 25년 동안 잘못된 것으로 여겨져 왔으며 여전히 코드를 이해하게 어렵게 만드는 주범 가운데 하나다.

깊은 중첩 구조를 피하는 것은 어렵지 않다. if 절과 else 절에서 수행되는 테스트를 재설계하거나 더 간단한 루틴으로 리팩터링하면 된다. 다음은 중첩 구조의 깊이를 줄이기 위한 방법을 소개한다.

  • 조건의 일부분을 다시 테스트하여 중첩된 if 문을 단순화하라
    if ( inputStatus == InputStatus_Success ) {
      // lots of code
    
      if ( printerRoutine != NULL ) {
          // lots of code
    
          if ( SetupPage() ) {
              // lots of code
                
              if ( AllocMem( &printData ) ) {
                   // lots of code
              }
          }
      }
    } 
    
if ( inputStatus == InputStatus_Success ) {
    // lots of code

    if ( printerRoutine != NULL ) {
        // lots of code

    }
}

if ( ( inputStatus == InputStatus_Success ) &&
    ( printerRoutine != NULL ) && SetupPage() ) {
    // lots of code

    if ( AllocMem( &printData ) ) {
        // lots of code
    }
}

중첩 수준을 대가 없이 줄일 수 없다는 면에서 현실적인 예제이다. 중첩 구조의 깊이를 줄이는 대가로 더 복잡한 테스트 코드를 작성해야 하지만 가족성이 크게 향상되기 때문에 고려한 가치가 있다.

  • break 블록을 사용하여 중첩된 if 문을 단순화하라.

  • 중첩된 if 문을 if-then-else 문으로 변환하라.

if ( 10 < quantity ) {
    if ( 100 < quantity ) {
        if ( 1000 < quantity ) {
            discount = 0.10;
        }
        else {
            discount = 0.05;
        }
    }
    else {
        discount = 0.025;
    }
}
else {
    discount = 0.0;
}

----

if ( 1000 < quantity ) {
    discount = 0.10;
}
else if ( 100 < quantity ) {
    discount = 0.05;
}
else if ( 10 < quantity ) {
    discount = 0.025;
}
else {
    discount = 0;
}
  • 중첩된 if 문을 case 문으로 변환하라 경우에 따라 어떤 비교문은 case 문을 사용하여 작성할 수 있다.
    Select Case quantity
      Case 0 To 10
          discount = 0.0
      Case 11 To 100
          discount = 0.025
      Case 101 To 1000
          discount = 0.05
      Case Else
          discount = 0.10
    End Select
    
  • 중첩 구조가 깊은 코드를 루틴으로 작성하라.
    while ( !TransactionsComplete() ) {
      // read transaction record
      transaction = ReadTransaction();
      // process transaction depending on type of transaction
      if ( transaction.Type == TransactionType_Deposit ) {
          // process a deposit
          if ( transaction.AccountType == AccountType_Checking ) {
              if ( transaction.AccountSubType == AccountSubType_Business )
                  MakeBusinessCheckDep( transaction.AccountNum, transaction.Amount );
              else if ( transaction.AccountSubType == AccountSubType_Personal )
                  MakePersonalCheckDep( transaction.AccountNum, transaction.Amount );
              else if ( transaction.AccountSubType == AccountSubType_School )
                  MakeSchoolCheckDep( transaction.AccountNum, transaction.Amount );
          }
          else if ( transaction.AccountType == AccountType_Savings )
              MakeSavingsDep( transaction.AccountNum, transaction.Amount );
          else if ( transaction.AccountType == AccountType_DebitCard )
              MakeDebitCardDep( transaction.AccountNum, transaction.Amount );
          else if ( transaction.AccountType == AccountType_MoneyMarket )
              MakeMoneyMarketDep( transaction.AccountNum, transaction.Amount );
          else if ( transaction.AccountType == AccountType_Cd )
              MakeCDDep( transaction.AccountNum, transaction.Amount );
      }
      ...
    

    루틴을 생성해 루틴을 호출하는 정도.

  • 좀 더 객체지향적인 방법으로 접근하라.

서브 클래스를 생성하여 단순화 한다.

TransactionData transactionData;
Transaction *transaction;
while ( !TransactionsComplete() ) {
    // read transaction record
    transactionData = ReadTransaction();
    // create transaction object, depending on type of transaction
    switch ( transactionData.Type ) {
        case ( TransactionType_Deposit ):
            transaction = new Deposit( transactionData );
            break;
        case ( TransactionType_Withdrawal ):
            transaction = new Withdrawal( transactionData );
            break;
        case ( TransactionType_Transfer ):
            transaction = new Transfer( transactionData );
            break;
        default:
            // process unknown transaction type
            LogTransactionError("Unknown Transaction Type", transactionData );
            return;
    }
    transaction->Complete();
    delete transaction;
}
  • 깊게 중첩된 코드를 재설계하라. 어떤 전문가들은 객체지향 프로그래밍에서 case 문이 사용되면 코드가 제대로 팩토링되지 않았다는 의미로 해석한다. 코드가 복잡하다는 것은 코드를 단순하게 작성할 수 잇을 만큼 프로그램을 충분히 이해하지 못한다는 것을 의미한다. 깊은 중첩 구조는 루틴을 분리하거나 복잡한 코드의 일부분을 재설계할 필요가 있다는 경고다.

19.5 프로그래밍의 기초: 구조적 프로그래밍

구조적 프로그래밍 이라는 용어는 1969 NATO의 소프트웨어공학 컨퍼런스에서 에츠허르 데이크스트라가 발표한 “구조적 프로그래밍” 논문에서 유래했다. 초기에는 구조적 설계, 구조적 분석, 구조적 농땡이 등 모든 소프트웨어 개발 행위에 적용했다.

구조적 프로그래밍의 핵심을 오직 하나의 입구와 출구만이 있는 제어 구조를 사용해야 한다는 간단한 개념이다. 단일 진입, 출발점 제어 구조는 다른 진입점이나 탈출점은 제시하지 않으며, 상세 코드 작성 단계에만 적용한다.

구조적 프로그래밍은 순차적이고 체계적인 방법으로 실행된다. 프로그램을 하향식으로 읽을 수 있고 거의 같은 방법으로 실행된다.

구조적 프로그래밍의 세 가지 요소

  • 순차적 명령문 순서대로 실행되는 명령문의 집합으로 할당문과 함수 호출이 있다.

  • 선택 선택은 명령문을 선택적으로 수행하는 제어 구조를 말한다. if-then-else 문이 흔하다. 각 절은 하나만 실행되며 동시에 실행되지는 않는다. case 문 또한 선택 제어의 또 다른 예다.

  • 반복 반복은 명령문 그룹을 여러 번 실행하는 제어 구조다. 반복은 일반적으로 반복문으로 알려져있다.

구조적 프로그래밍 이론의 핵심은 모든 제어 구조를 순서, 선택, 반복이라는 세 가지 구성 요소로부터 만들 수 있다는 것이다. 개발자들은 편이한 언어 구조를 좋아하지만 프로그래밍은 프로그래밍 언어로 할 수 있는 편의를 제한함으로써 크게 발전해 온 것 같다. goto와 같은 사용하기는 편리하지만 이해할 수 없어 유지보수가 불가능한 것과 break, continue, throw-catch 등의 다른 제어 구조를 사용할 때는 비판적 시각으로 바라봐야 한다고 믿는다.

19.6 제어 구조와 복잡성

제어 구조에 주의를 기울여야 하는 이유의 하나는 제어 구조가 프로그램의 전체 복잡도에 지대한 영향을 미치기 때문이다.

프로그래밍 복잡도를 측정하는 한 가지 방법은 프로그램을 이해하기 위해서 기억해야 하는 객체의 수를 세어보는 것이다. 가장 어려운 부분의 하나이며 동시에 프로그래밍이 다른 활동보다 더 많은 집중력이 요구되는 이유이기도 하다.

프로그램의 복잡성은 프로그램을 이해하기 위해 얼마만큼 노력해야 하는지 결정하는데 중대한 역할을 하는 것 같다. 톰 맥케이브는 프로그램의 복잡도가 제어 흐름에 의해 정의된다고 주장하는 논문을 발표했다.

복잡도가 얼마나 중요한가?

에츠허르 데이크스트라는 “유능한 개발자는 자신의 두뇌에 한계가 있다는 사실을 잘 알고 있어서 겸손한 자세로 프로그래밍 작업에 임한다.”고 했다. 이 말은 두뇌 능력을 키워야 한다는 이야기가 아니라, 복잡도가 대단히 높은 코드를 절대로 다룰 수 없기 때문에 가능한 한 복잡도를 줄이기 위한 단계를 따라야 한다는 뜻이다.

복잡도를 줄이기 위한 일반적인 방법

다음 두 가지 중 한 가지 방법으로 복잡도를 처리할 수 있다.

  1. 두뇌 훈련을 통해서 두뇌의 처리 능력을 향상시킨다.
  2. 프로그램의 복잡도와 프로그램을 이해하기 위해 집중해야 하는 대상의 수를 줄이는 방법이 있다.

복잡도 측정법

영향력있는 수치 계산 기법은 톰 맥케이브의 방법이다. 루틴 안의 “의사결정 지점”의 수를 세어 복잡도를 측정한다.

1. Start with 1 for the straight path through the routine.
2. Add 1 for each of the following keywords, or their equivalents: if while repeat for
and or
3. Add 1 for each case

복잡도 측정의 활용

0–5 The routine is probably fine.
6–10 Start to think about ways to simplify the routine.
10+ Break part of the routine into a second routine and call it from the first
routine

루틴의 일부분을 다른 루틴으로 만든다고 하더라도 의사결정 지점을 이동시킬 뿐 프로그램의 전체적인 복잡도는 감소하지 않는다. 그러나 한 번에 다루어야 하는 복잡도가 줄어들어 루틴의 복잡도를 줄이는 것이 가치가 있다.