본문 바로가기
개발

[리팩터링 2판] JavaScript 리팩터링 도서학습 #6

by 개발과 운동, 그리고 책장 2025. 5. 17.


해당 내용은 위의 책의 내용을 제가 이해한대로 정리해둔 내용입니다.

이번 포스팅에선 Chapter10 의 내용에 대해 다룹니다. 

 

  • Chapter10 조건부 로직 간소화 

10장에서 소개하는 주요 리팩터링 기법들을 요약하고 예제와 함께 정리했습니다.

Chapter10 조건부 로직 간소화 

1.조건문 분해하기
2.중복 조건식 통합하기
3.중첩 조건문을 보호 구문으로 바꾸기
4.조건부 로직을 다형성으로 바꾸기
5.특이 케이스 추가하기
6.어서션 추가하기
7.제어 플래그를 탈출문으로 바꾸기

 

조건부 로직 간소화의 효과

복잡한 조건부 로직은 코드 이해와 유지보수를 어렵게 합니다

조건부 로직을 간소화하면 얻을수있는 이점 

 

  • 가독성 향상: 코드의 의도가 명확해져 이해하기 쉬워집니다
  • 유지보수성 개선: 간결한 조건문은 변경 사항 적용이 쉽습니다
  • 버그 감소: 복잡한 조건문에서 발생하기 쉬운 논리적 오류를 줄입니다
  • 확장성 향상: 더 명확한 구조로 기능 추가가 용이해집니다
  • 테스트 용이성: 단순한 조건문은 테스트 케이스 작성이 쉽습니다

조건부 로직 간소화 기법

1. 조건문 분해하기

// Before
function calculateCharge(date, quantity, plan) {
  let charge = 0;
  if (!date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd)) {
    charge = quantity * plan.summerRate;
  } else {
    charge = quantity * plan.regularRate + plan.regularServiceCharge;
  }
  return charge;
}

// After
function calculateCharge(date, quantity, plan) {
  if (isSummer(date, plan)) {
    return summerCharge(quantity, plan);
  } else {
    return regularCharge(quantity, plan);
  }
}

function isSummer(date, plan) {
  return !date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd);
}

function summerCharge(quantity, plan) {
  return quantity * plan.summerRate;
}

function regularCharge(quantity, plan) {
  return quantity * plan.regularRate + plan.regularServiceCharge;
}

 

2. 중복 조건식 통합하기

  • 여러 조건문이 동일한 결과를 낼 때 하나의 조건식으로 통합
  • 코드 중복 제거, 의도 명확화, 수정 시 발생할 수 있는 불일치 방지
// Before
function disabilityAmount(employee) {
  if (employee.seniority < 2) return 0;
  if (employee.monthsDisabled > 12) return 0;
  if (employee.isPartTime) return 0;
  // 장애 수당 계산 로직
  return calculateDisabilityAmount(employee);
}

// After
function disabilityAmount(employee) {
  if (isNotEligibleForDisability(employee)) return 0;
  // 장애 수당 계산 로직
  return calculateDisabilityAmount(employee);
}

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

 

3. 중첩 조건문을 보호 구문으로 바꾸기

  • 여러 조건문이 동일한 결과를 낼 때 하나의 조건식으로 통합
  • 코드 중복 제거, 의도 명확화, 수정 시 발생할 수 있는 불일치 방지
// Before
function getPayAmount(employee) {
  let result = 0;
  if (employee.isSeparated) {
    result = { amount: 0, reasonCode: "SEP" };
  } else {
    if (employee.isRetired) {
      result = { amount: 0, reasonCode: "RET" };
    } else {
      // 급여 계산 로직
      result = someFinalComputation();
    }
  }
  return result;
}

// After
function getPayAmount(employee) {
  // 보호 구문으로 예외 케이스 먼저 처리
  if (employee.isSeparated) return { amount: 0, reasonCode: "SEP" };
  if (employee.isRetired) return { amount: 0, reasonCode: "RET" };
  
  // 주요 로직이 중첩 없이 명확하게 드러남
  return someFinalComputation();
}

 

4. 조건부 로직을 다형성으로 바꾸기

  • 타입에 따른 조건부 로직을 클래스와 다형성을 사용하여 대체
  • 새로운 타입 추가가 용이해지고, 조건부 로직이 관련 클래스로 분산되어 응집도 향상
// Before
function plumages(birds) {
  return birds.map(b => plumage(b));
}

function speeds(birds) {
  return birds.map(b => airSpeedVelocity(b));
}

function plumage(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return "average";
    case 'AfricanSwallow':
      return (bird.numberOfCoconuts > 2) ? "tired" : "average";
    case 'NorwegianBlueParrot':
      return (bird.voltage > 100) ? "scorched" : "beautiful";
    default:
      return "unknown";
  }
}

function airSpeedVelocity(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return 35;
    case 'AfricanSwallow':
      return 40 - 2 * bird.numberOfCoconuts;
    case 'NorwegianBlueParrot':
      return (bird.isNailed) ? 0 : 10 + bird.voltage / 10;
    default:
      return null;
  }
}

// After
function plumages(birds) {
  return birds.map(b => createBird(b).plumage);
}

function speeds(birds) {
  return birds.map(b => createBird(b).airSpeedVelocity);
}

function createBird(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return new EuropeanSwallow(bird);
    case 'AfricanSwallow':
      return new AfricanSwallow(bird);
    case 'NorwegianBlueParrot':
      return new NorwegianBlueParrot(bird);
    default:
      return new Bird(bird);
  }
}

class Bird {
  constructor(birdObject) {
    Object.assign(this, birdObject);
  }
  
  get plumage() {
    return "unknown";
  }
  
  get airSpeedVelocity() {
    return null;
  }
}

class EuropeanSwallow extends Bird {
  get plumage() {
    return "average";
  }
  
  get airSpeedVelocity() {
    return 35;
  }
}

class AfricanSwallow extends Bird {
  get plumage() {
    return (this.numberOfCoconuts > 2) ? "tired" : "average";
  }
  
  get airSpeedVelocity() {
    return 40 - 2 * this.numberOfCoconuts;
  }
}

class NorwegianBlueParrot extends Bird {
  get plumage() {
    return (this.voltage > 100) ? "scorched" : "beautiful";
  }
  
  get airSpeedVelocity() {
    return (this.isNailed) ? 0 : 10 + this.voltage / 10;
  }
}

 

5. 특이 케이스 추가하기

  • null이나 예외적인 값을 처리하는 반복적인 코드를 특이 케이스 객체로 대체
  • 예외 처리 로직의 중복 제거, 코드 이해도 향상, 특수 상황 처리 일관성 확보
// Before
class Site {
  constructor(customer) {
    this._customer = customer;
  }
  
  get customer() { return this._customer; }
}

// 클라이언트 코드
const site = new Site(customer);
const customerName = site.customer ? site.customer.name : "occupant";
const plan = site.customer ? site.customer.plan : registry.basicPlan;
const weeksDelinquent = site.customer ? site.customer.paymentHistory.weeksDelinquentInLastYear : 0;

// After
// 특이 케이스 객체 생성
class UnknownCustomer {
  get name() { return "occupant"; }
  get plan() { return registry.basicPlan; }
  get paymentHistory() { return new NullPaymentHistory(); }
  
  // 특이 케이스 확인 메서드
  get isUnknown() { return true; }
}

class NullPaymentHistory {
  get weeksDelinquentInLastYear() { return 0; }
}

class Customer {
  get isUnknown() { return false; }
  // 기존 Customer 코드...
}

class Site {
  constructor(customer) {
    this._customer = (customer === "unknown") ? new UnknownCustomer() : customer;
  }
  
  get customer() { return this._customer; }
}

// 클라이언트 코드 단순화
const site = new Site(customer);
const customerName = site.customer.name;
const plan = site.customer.plan;
const weeksDelinquent = site.customer.paymentHistory.weeksDelinquentInLastYear;

// 더 명시적인 구분이 필요한 경우
if (site.customer.isUnknown) {
  // 알 수 없는 고객에 대한 특별 처리
}

 

6. 어서션 추가하기

  • 코드가 특정 조건을 만족한다고 가정하는 부분에 명시적 검사를 추가
  • 의도와 가정을 명확히 표현, 오류를 빠르게 발견, 디버깅 용이성 향상
// Before
function applyDiscount(customer, amount) {
  // 할인율이 양수라고 가정하지만 명시적 검사 없음
  return amount - (amount * customer.discountRate);
}

// After
function applyDiscount(customer, amount) {
  // 명시적 어서션으로 가정 검증
  assert(customer.discountRate >= 0, "할인율은 음수가 될 수 없습니다");
  return amount - (amount * customer.discountRate);
}

// 기본 어서션 함수
function assert(condition, message) {
  if (!condition) {
    throw new Error(message || "Assertion failed");
  }
}

 

7. 제어 플래그를 탈출문으로 바꾸기

  • 루프를 제어하는 플래그 변수를 break, continue, return 등의 탈출문으로 대체
  • 코드 단순화, 가독성 향상, 논리 구조 명확화
// Before
function findPerson(people) {
  let found = false;
  for (let i = 0; i < people.length; i++) {
    if (!found) {
      if (people[i] === "Don") {
        sendAlert();
        found = true;
      }
      if (people[i] === "John") {
        sendAlert();
        found = true;
      }
    }
  }
}

// After
function findPerson(people) {
  for (let i = 0; i < people.length; i++) {
    if (people[i] === "Don") {
      sendAlert();
      return;
    }
    if (people[i] === "John") {
      sendAlert();
      return;
    }
  }
}

// 더 단순하게 - 현대적 JavaScript 방식
function findPerson(people) {
  const targetPeople = ["Don", "John"];
  const found = people.find(p => targetPeople.includes(p));
  if (found) sendAlert();
}

 

마무리

조건부 로직 간소화 기법들을 학습하면서 깨달은 가장 큰 점은, 복잡한 조건문이 그 자체로 코드의 '악취'라는 사실입니다. 프론트엔드 개발을 하며 가장 자주 마주했던 문제이기도했고, 특히 레거시 코드를 인수받거나 대규모 프로젝트에 투입되었을 때, 깊게 중첩된 if-else 구문과 거대한 switch 문은 코드 이해의 많은 문제가 되었습니다. 

// Before
function processUserRegistration(user) {
  if (user) {
    if (user.age >= 18) {
      if (user.email && validateEmail(user.email)) {
        if (!isBlacklisted(user.email)) {
          // 여기에 핵심 로직 - 50줄 이상의 복잡한 코드
          return SUCCESS;
        } else {
          return ERROR_BLACKLISTED;
        }
      } else {
        return ERROR_INVALID_EMAIL;
      }
    } else {
      return ERROR_UNDERAGE;
    }
  } else {
    return ERROR_NO_USER;
  }
}

// After
function processUserRegistration(user) {
  // 보호 구문으로 변환
  if (!user) return ERROR_NO_USER;
  if (user.age < 18) return ERROR_UNDERAGE;
  if (!user.email || !validateEmail(user.email)) return ERROR_INVALID_EMAIL;
  if (isBlacklisted(user.email)) return ERROR_BLACKLISTED;
  
  // 여기서부터 핵심 로직에 집중할 수 있음
  // 조건 체크에서 벗어나 비즈니스 로직이 명확하게 보임
  return SUCCESS;
}



JavaScript의 컨텍스트에서는 함수형 프로그래밍 패러다임이 조건부 로직을 간소화하는 데 특히 효과적이라고 생각합니다. 조건들을 작은 함수로 분리하고 조합하는 방식은 가독성과 테스트 용이성을 크게 높여줍니다.

 

// 조건들을 명확한 의미를 가진 작은 함수로 분리
const meetsAgeRequirement = user => user.age >= 18;
const hasValidEmail = user => user.email && validateEmail(user.email);
const isAllowedToRegister = user => !isBlacklisted(user.email);

// 조건 함수들을 조합하여 사용
const canRegister = user => 
  user && 
  [meetsAgeRequirement, hasValidEmail, isAllowedToRegister]
    .every(check => check(user));

// 메인 로직이 훨씬 명확해짐
function processUserRegistration(user) {
  if (!canRegister(user)) {
    return generateErrorCode(user); // 적절한 에러 코드를 생성하는 함수
  }
  
  // 핵심 비즈니스 로직
}

 

 

결국 조건부 로직 간소화는 단순히 코드를 "예쁘게" 만드는 것이 아니라,

비즈니스 로직의 본질을 더 명확하게 드러내고 변화에 유연하게 대응할 수 있는 코드 구조를 만드는 과정입니다.


복잡한 조건문 하나를 리팩터링하는 데 시간을 투자하는 것이, 장기적으로는 그 몇 배의 시간을 절약해 준다라고 하는데, 이러한 리팩터링을 통해서 많은 시간이 절약되는 경험을 빠른시일내에 마주할수있기를 바랍니다. 

댓글