Chapter 10

조건부 로직 간소화

  • 10.1 조건문 분해하기
  • 10.2 조건식 통합하기
  • 10.3 중첩 조건문을 보호 구문으로 바꾸기
  • 10.4 조건부 로직을 다형성으로 바꾸기
  • 10.5 특이 케이스 추가하기
  • 10.6 어서션 추가하기
  • 10.7 제어 플래그를 탈출문으로 바꾸기

조건문은 프로그래밍의 필수 요소지만, 복잡한 조건부 로직은 코드를 읽기 어렵게 만드는 주범이야. 10장은 조건부 로직을 깔끔하게 만드는 기법들을 다뤄. 이걸 적용하면 조건문의 의도가 훨씬 명확해져.

**조건문 분해하기(Decompose Conditional)**는 복잡한 조건문의 조건, then 절, else 절을 각각 함수로 추출하는 거야.

// Before
if (date.before(SUMMER_START) || date.after(SUMMER_END))
  charge = quantity * winterRate + winterServiceCharge;
else
  charge = quantity * summerRate;

// After
if (isSummer(date))
  charge = summerCharge(quantity);
else
  charge = winterCharge(quantity);

"조건이 왜 분기하는지(무엇을 검사하는지)와 분기했을 때 무엇을 하는지를 분리하면 코드의 의도가 드러난다." 조건 부분을 함수로 추출하면 "언제"가 명확해지고, 각 분기를 함수로 추출하면 "무엇을"이 명확해져.

**조건식 통합하기(Consolidate Conditional Expression)**는 같은 결과를 반환하는 조건 검사가 여러 개 나열되어 있으면 하나로 합치는 거야.

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

// After
if (isNotEligibleForDisability()) return 0;

조건을 합치면 "이 검사들이 하나의 의미를 가진다"는 것이 코드에 드러나. 다만 정말로 독립적인 검사라면 합치지 마. 우연히 같은 결과를 내는 것과 논리적으로 하나인 것은 다르거든.

**중첩 조건문을 보호 구문으로 바꾸기(Replace Nested Conditional with Guard Clauses)**는 깊이 중첩된 if-else를 평탄화하는 거야.

// 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();
}

if-else 구조는 "두 경우가 동등하게 중요하다"는 의미를 내포해. 하지만 실제로는 "이 조건이면 빨리 빠져나가고, 핵심 로직은 아래에 있다"는 구조가 더 많잖아. 보호 구문은 **"이 조건이면 더 이상 볼 필요 없다"**는 메시지를 명확히 전달해. 함수의 반환점이 하나여야 한다는 규칙? 파울러는 동의하지 않아. 의도가 드러나면 반환점이 여러 개여도 괜찮다는 거지.

**조건부 로직을 다형성으로 바꾸기(Replace Conditional with Polymorphism)**는 1장에서 이미 봤어. 타입에 따라 분기하는 switch문이나 if-else 체인을 서브클래스의 오버라이드로 대체하는 거야. "모든 조건문을 다형성으로 바꿔야 한다는 건 아니다." 단순한 조건문은 그 자체로 충분히 명확해. 다형성이 빛을 발하는 건 같은 조건 분기가 여러 곳에 반복될 때야.

**특이 케이스 추가하기(Introduce Special Case)**는 null 검사나 특정 값에 대한 분기가 코드 곳곳에 반복되면 특이 케이스 객체로 대체하는 거야.

// Before — 곳곳에 null 체크
if (customer === "미확인 고객") plan = registry.billingPlans.basic;
if (customer === "미확인 고객") name = "거주자";

// After — 특이 케이스 객체
class UnknownCustomer {
  get billingPlan() { return registry.billingPlans.basic; }
  get name() { return "거주자"; }
}

특이 케이스 객체가 일반 객체와 같은 인터페이스를 구현하니까, 호출하는 쪽에서 분기할 필요가 없어. 특이 케이스 처리 로직이 한 곳에 모이니까 변경도 쉬워지고.

**어서션 추가하기(Introduce Assertion)**는 코드가 특정 조건을 당연히 참이라고 가정하고 있을 때, 그 가정을 어서션으로 명시하는 거야. 어서션은 소통 도구야. **"이 조건은 항상 참이어야 한다"**는 걸 코드로 표현해. 프로그래머의 실수를 잡는 데 쓰는 거지, 사용자 입력 검증에 쓰는 게 아니야.

**제어 플래그를 탈출문으로 바꾸기(Replace Control Flag with Break)**는 found 같은 불리언 플래그로 반복문의 흐름을 제어하는 코드를 break, continue, return 같은 탈출문으로 바꾸는 거야.

// Before
let found = false;
for (const p of people) {
  if (!found) {
    if (p === "악당") {
      sendAlert();
      found = true;
    }
  }
}

// After
for (const p of people) {
  if (p === "악당") {
    sendAlert();
    break;
  }
}

제어 플래그를 쓰면 "이 반복문이 언제 끝나는 거지?"를 추적해야 하는데, 탈출문은 "여기서 끝난다"를 직접적으로 말해줘.


정리

10장 읽고 기억할 거 세 가지:

  1. "조건문을 분해하면 '왜 분기하는가'와 '무엇을 하는가'가 분리된다." 의도가 드러나는 코드가 돼
  2. "보호 구문으로 중첩을 제거하라." 반환점이 여러 개여도 괜찮아. 의도가 드러나는 게 더 중요해
  3. "특이 케이스 객체로 null 검사를 없애라." 분기가 사라지고, 특이 케이스 로직이 한 곳에 모여