10장, 조건부 로직 간소화

2021.11.01

1. Deocompose Conditional, 조건문 분해하기

  • 복잡한 조건부 로직은 프로그램을 복잡하게 만드는 가장 흔한 원흉에 속한다.
  • 코드를 이해하기 쉽게 거대한 코드 블록을 부위별로 분해해 적절한 이름을 가진 함수로 추출하자.

절차

  1. 조건식과 그 조건식에 딸린 조건절 각각을 함수로 추출한다.

예시

before

if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) change = quantity * plan.summerRate;
else change = quantity * plan.regularRate + plan.regularServiceCharge;

after

if (summer()) charge = summerCharge();
else charge = regularCharge();

2. Consolidate Conditional Expression, 조건식 통합하기

  • 비교하는 조건은 다르지만 그 결과로 수행하는 동작은 똑같은 코드들은 조건 검사도 하나로 통합하는게 낫다. ( and 혹은 or 연산자를 사용 )
    • 여러 조각으로 나뉜 조건들을 하나로 통합함으로써 하려는 일이 더 명확해진다.
    • 이 과정에서 함수로 추출까지 할 수 있다.
  • 독립된 조건 검사들이라면 통합하면 안된다.

절차

  1. 해당 조건식들 모두 부수효과가 없는지 확인한다.
  2. 조건문 두 개를 선택하여 두 조건문의 조건식들을 논리 연산자로 결합한다.
  3. 테스트.
  4. 조건이 하나만 남을 때까지 2~3과정을 반복한다
  5. 하나로 합쳐진 조건식을 함수로 추출할지 고려해본다

예시

before

if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthsDisabled > 12) return 0;
if (anEmployee.isPartTime) return 0;

after

if (isNotEligibleForDisability()) return 0;

function isNotEligibleForDisability() {
  return anEmployee.seniority < 2 || anEmployee.monthsDisabled > 12 || anEmployee.isPartTime;
}

3. Replace Nested Conditional with Guard Clauses, 중첩 조건문을 보호 구문으로 바꾸기

  • 조건문은 보통 두 가지 형태로 쓰인다.

    • 참인 경로와 거짓인 경로 모두 정상 동작으로 이어지는 형태.

      if와 else절을 사용

    • 한쪽만 정상인 형태.

      한쪽만 정상이라면 비정상 조건을 if에서 검사한 다음, 조건이 참이면(비정상이면) 함수에서 빠져나온다. ( guard clause 보호 구문 )

  • 함수의 진입점은 하나로 강제되지만, 반환점은 하나일수 도, 여러개 일 수도 있다.

    • 로직이 더 명확하다면 반환점을 하나로 만들고, 그렇지 않다면 굳이 하나로 만들지 않아도 된다.

절차

  1. 교체해야 할 조건 중 가장 바깥 것을 선택하여 보호 구문으로 바꾼다.
  2. 테스트.
  3. 1~2과정을 필요한 만큼 반복한다.
  4. 모든 보호 구문이 같은 결과를 반환한다면 보호 구문들의 조건식을 통합한다.

예시

before

function getPayAmount() {
  let result;
  if (isDead) result = deadAmount();
  else {
    if (isSeparated) result = separatedAmount();
    else {
      if (isRetired) result = retiredAmount();
      else result = normalPayAmount();
    }
  }
  return result;
}

after

function getPayAmount() {
  if (isDead) return deadAmount();
  if (isSeparated) return separatedAmount();
  if (isRetired) return retiredAmount();
  return normalPayAmount();
}

4. Replace Conditional with Polymorphism, 조건부 로직을 다형성으로 바꾸기

  1. 복잡한 조건부 로직은 직관적으로 구조화하면 코드가 조금 더 명확해진다.
  2. 다형성은 객체 지향 프로그래밍의 핵심이지만 남용하기 쉽다.
  3. 모든 조건부 로직을 다형성으로 대체할 필요는 없다.

절차

  1. 다형적 동작을 표현하는 클래스들이 아직 없다면 만들어준다. 이왕이면 적합한 인스턴스를 알아서 만들어 반환하는 팩터리 함수도 함께 만든다.
  2. 호출하는 코드에서 팩터리 함수를 사용하게 한다.
  3. 조건부 로직 함수를 슈퍼클래스로 옮긴다.
  4. 서브클래스 중 하나를 선택한다. 서브클래스에서 슈퍼클래스의 조건부 로직 메서드를 오버라이드한다. 조건부 문장 중 선택된 서브클래스에 해당하는 조건절을 서브클래스 메서드로 복사한 다음 적절히 수정한다.
  5. 같은 방식으로 각 조건절을 해당 서브클래스에서 메서드로 구현한다.
  6. 슈퍼클래스 메서드에는 기본 동작 부분만 남긴다. 혹은 슈퍼클래스가 추상 클래스여야 한다면, 이 메서드를 추상으로 선언하거나 서브클래스에서 처리해야 함을 알리는 에러를 던진다.

예시

before

switch (bird.type) {
  case '유럽 제비':
    return '보통이다';
  case '아프리카 제비':
    return bird.numberOfCounts > 2 ? '지쳤다' : '보통이다';
  case '노르웨이 파랑 앵무':
    return bird.voltage > 100 ? '그을렸다' : '예쁘다';
  default:
    return '알 수 없다';
}

after

class EuropeanSwallow {
  get plumage() {
    return '보통이다';
  }
  // ...
}

class AfricanSwallow {
  get plumage() {
    return bird.numberOfCounts > 2 ? '지쳤다' : '보통이다';
  }
  // ...
}

class NorwegianBlueParrot {
  get plumage() {
    return bird.voltage > 100 ? '그을렸다' : '예쁘다';
  }
  // ...
}

5. Introduce Special Case, 특이 케이스 추가하기

  • 데이터 구조의 특정 값을 확인한 후 똑같은 동작을 수행하는 코드가 곳곳에 등장하는 경우가 더러 있있는데, 이러한 공통 동작을 한곳에 모으는게 효율적이다.
  • 특이 케이스를 확인하는 코드 대부분을 단순한 함수 호출로 바꿀 수 있다.
  • 널은 특이 케이스로 처리해야 할 때가 많다. ( 널 객체 패턴 )

절차

  1. 컨테이너에 특이 케이스인지를 검사하는 속성을 추가하고, false를 반환하게 한다.
  2. 특이 케이스 객체를 만든다. 이 객체는 특이 케이스인지를 검사하는 속성만 포함하며, 이 속성은 true를 반환하게 한다.
  3. 클라이언트에서 특이 케이스인지를 검사하는 코드를 함수로 추출한다. 모든 클라이언트가 값을 직접 비교하는 대신 방금 추출한 함수를 사용하도록 고친다.
  4. 코드에 새로운 특이 케이스 대상을 추가한다. 함수의 반환 값으로 받거나 변환 함수를 적용하면 된다.
  5. 특이 케이스를 검사하는 함수 본문을 수정하여 특이 케이스 객체의 속성을 사용하도록 한다.
  6. 테스트.
  7. 여러 함수를 클래스로 묶기나 여러 함수를 변환 함수로 묶기를 적용하여 특이 케이스를 처리하는 공통 동작을 새로운 요소로 옮긴다.
  8. 아직도 특이 케이스 검사 함수를 이용하는 곳이 남아 있다면 검사 함수를 인라인한다.

예시

before

if (aCustomer === '미확인 고객') customerName = '거주자';

after

class UnknownCustomer {
  get name() {
    return '거주자';
  }
}

6. Introduce Assertion, 어서션 추가하기

  • 특정 조건이 참일 때만 제대로 동작하는 코드 영역이 있을 수 있다.
    • 이런 가정은 코드에 항상 명시적으로 기술되어 있지 않아서, 어서션(단언)을 이용하여 코드 자체에 삽입해 놓으면 좋다.
  • 어서션은 항상 참이라고 가정하는 조건부 문장으로, 어서션이 실패했다는 건 프로그래머가 잘못했다는 뜻이다.
  • 어서션이 있고 없고가 프로그램 기능의 정상 동작에 아무런 영향을 주지 않도록 작성되어야 한다.
  • 테스트 코드가 있다면 어서션의 디버깅 용도로서의 효용은 줄어든다.
  • 어서션을 남발하는 것 역시 위험하다.
    • 반드시 참이야만 하는 것만 검사.
    • 프로그래머가 일으킬만한 오류에만 사용.

절차

  1. 참이라고 가정하는 조건이 보이면 그 조건을 명시하는 어서션을 추가한다.

예시

before

if (this.discountRate) base = base - this.discountRate * base;

after

assert(this.discountRate >= 0);
if (this.discountRate) base = base - this.discountRate * base;

7. Replace Control Flag with Break, 제어 플래그를 탈출문으로 바꾸기

  • 제어 플래그란 코드의 동작을 변경하는 데 사용되는 변수를 말하며, 어딘가에서 값을 계산해 제어 플래그에 설정한 후 다른 어딘가의 조건문에서 검사하는 형태로 쓰인다.
  • 리팩터링으로 충분히 간소화할 수 있음에도 복잡하게 작성된 코드에서 나타나기 때문에, 악취나는 코드로 본다.
  • 제어 플래그의 주 서식지는 반복문 안이다. break문이나 continue문 활용에 익숙하지 않은 사람이 심어놓기도 하고, 함수의 return문을 하나로 유지하고자 노력하는 사람이 심기도 한다.

절차

  • 제어 플래그를 사용하는 코드를 함수로 추출할지 고려한다.
  • 제어 플래그를 갱신하는 코드 각각을 적절한 제어문으로 바꾼다. 하나 바꿀 때마다 테스트.
  • 모두 수정했다면 제어 플래그를 제거한다.

예시

before

for (const p of people) {
  if (!found) {
    if (p === '조커') {
      sendAlert();
      found = true;
    }
    // ...
  }
}

after

for (const p of people) {
  if (p === '조커') {
    sendAlert();
    break;
  }
  // ...
}