티스토리 뷰

조건부 로직은 프로그램을 복잡하게 만드는 주요 원흉이다. 따라서, 이해하기 쉽게 바꾸는 리팩터링이 필요하다.

 

  • 복잡한 조건문은
    • 10.1 조건문 분해하기
  • 논리적 조합을 명확하게 다듬는
    • 10.2 중복 조건식 통합하기
  • 핵심 로직에 앞서 무언가를 검사해야 할 때
    • 10.3 중첩 조건문을 보호 구문으로 바꾸기
  • 똑같은 분기 로직이 여러 곳에 등장한다면
    • 10.4 조건부 로직을 다형성으로 바꾸기
  • null 과 같은 특이 케이스의 처리 로직이 거의 똑같다면
    • 10.5 특이 케이스 추가하기 (널 객체 추가하기)
  • 특정 조건일때만 제대로 동작하는 코드가 있는 경우
    • 10.6 어서션 추가하기
  • 제어 플래그를 이용해 코드 동작 흐름을 변경하는 코드는
    • 10.7 제어 플래그를 탈출문으로 바꾸기

 

 

10.1 조건문 분해하기 (Decompose Conditional)

개요

  • 다양한 조건, 그에 따라 동작도 다양한 코드를 작성하면 꽤 긴 함수가 탄생하여 읽기 어렵게 된다
  • 거대한 조건식과 그 조건식에 딸린 조건절을 각각 함수로 추출하는 리팩터링이 필요하다

적용시점

  • 조건부 로직이 복잡해지는 경우

효과

  • 해당 조건이 무엇인지 강조하고, 무엇을 분기했는지 명확해진다

before

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

after

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

 

 

10.2 조건식 통합하기 (Consolidate Conditional Expression)

개요

  • 비교하는 조건은 다르지만 동작은 똑같은 경우 조건 검사도 하나로 통합하는게 낫다

적용시점

  • 동작이 똑같은 조건절이 여럿인 경우
  • 단, 하나의 검사라고 생각할 수 없는, 진짜로 독립된 검사들이라고 판단되면 이 리팩터링을 해서는 안 된다

효과

  • 여러 조각으로 나뉜 조건들을 통합하여 코드의 의도가 더 명확해진다
  • 이 리팩터링을 통해 함수 추출하기(6.1) 까지 이어 질 수 있다

before

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

after

  if (isNotEligableForDisability()) return 0;

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

 

 

10.3 중첩 조건문을 보호 구문으로 바꾸기 (Replace Nested Conditional with Guard Clauses)

개요

  • 비정상 조건인 경우 함수를 종료하는 것을 보호 구문(guard clause)이라고 한다 (Early Return 이라고도 함)
  • 보호 구문은 "이건 이 함수의 핵심이 아니다" 라는 의미를 담고 있어 함수의 핵심 의도를 부각시킬 수 있다
  • 함수의 반환점이 한다는 규칙은 정말이지 유용하지 않다. 코드에서는 명확함이 핵심이기 때문에 반환점이 하나일 때 로직이 더 명백할 경우에만 그렇게 하자

적용시점

  • 비정상 조건을 중첩하는 구문이 있는 경우

효과

  • 보호 구문을 통해 함수의 핵심 로직이 아닌 부분을 명확히 구분하고 불필요한 복잡도를 줄인다

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

 

 

10.4 조건부 로직을 다형성으로 바꾸기 (Replace Conditional with Polymorphism)

개요

  • 복잡한 조건부 로직은 프로그래밍에서 해석하기 가장 난해한 대상에 속함
  • 조건문 구조를 그대로 둔 채 해결될 떄도 있지만, 클래스와 다형성을 이용하면 더 확실하게 분리가 가능하다
  • 다형성은 객체 지향 프로그래밍의 핵심이다. 하지만, 모든 조건부 로직을 다형성으로 대체해야 한다는 견해에는 동의하지 않는다

적용시점

  • 타입을 기준으로 분기하는 switch 문이 포함된 함수가 여러개 보이는 경우
  • 남용하기 쉬우니 개선이 필요한 복잡한 조건부 로직인지를 먼저 판단해야 한다

효과

  • 기본 로직을 슈퍼클래스로 넣어서 서브클래스의 변형 동작에 신경 쓰지 않고 기본에 집중하게 할수 있게 한다.
  • switch 로직의 중복을 없앨 수 있다

before

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";

after

class EuropeanSwallow {
  get plumage() {
    return "average";
  }
class AfricanSwallow {
  get plumage() {
     return (this.numberOfCoconuts > 2) ? "tired" : "average";
  }
class NorwegianBlueParrot {
  get plumage() {
     return (this.voltage > 100) ? "scorched" : "beautiful";
  }

 

 

10.5 특이 케이스 추가하기 (Introduce Special Case)

개요

  • 데이터 구조 특정 값을 확인 후 똑같은 동작을 수행하는 코드가 곳곳에 등작하는 경우도 중복 코드 중 하나다
  • 널과 같은 특정값에 대해 동일한 반응들을 한데로 모으는게 효율적이다
  • 클래스 이용하기, 객체 리터럴 이용하기, 변환 함수 이용하기와 같은 다양한 처리 방식이 존재함

적용시점

  • 널 값과 같은 특이 케이스에 대한 똑같은 동작을 수행하는 코드가 곳곳에 등장하는 경우
  • 특이 케이스에 따라 전용 클래스 이용하기, 객체 리터럴 이용하기, 변환 함수 이용하기 방법을 적용한다

효과

  • 특이 케이스를 확인하고 처리하는 코드를 한 데 모아 효율적으로 관리할 수 있다

before

if (aCustomer === "unknown") customerName = "occupant";

after

class UnknownCustomer {
    get name() {return "occupant";}

 

 

10.6 어서션 추가하기 (Introduce Assertion)

개요

  • 특정 조건이 참일 때만 제대로 동작하는 가정이 코드에 항상 명시적으로 기술되어 있지는 않다
  • 어서션은 항상 참이라고 가정하는 조건부 문장으로 어서션이 실패했다는 건 프로그래머가 잘못했다는 뜻이다
  • 어서션이 있고 없고가 프로그램 기능의 정상 동작에 아무런 영향을 주지 않도록 작성되어야 한다

적용시점

  • 특정 조건이 참일 때만 제대로 동작하는 코드 영역이 있는 경우

효과

  • 프로그램이 어떤 상태임을 가정한 채 실행되는지를 다른 개발자에게 알려주는 훌륭한 소통 도구가 된다
  • 단, 테스트 코드가 있다면 어서션의 디버깅 용도로서의 효용은 줄어든다

before

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

after

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

 

 

10.7 제어 플래그를 탈출문으로 바꾸기 (Replace Control Flag with Break)

개요

  • 제어 플래그란 코드의 동작을 변경하는데 사용되는 변수이다
  • 이런 코드는 리팩터링으로 충분히 간소화할 수 있음에복잡하게 작성된 코드에서 흔히 나타난다

적용시점

  • 반복문안에서의 제어 플래그가 있는 경우

효과

  • 불필요하게 복잡하게 작성된 코드를 간소화 한다

before

for (const p of people) {
  if (! found) {
    if ( p === "Don") {
      sendAlert();
      found = true;
    }

after

for (const p of people) {
  if ( p === "Don") {
    sendAlert();
    break;
  }

 

 

참고
http://www.yes24.com/Product/Goods/89649360
https://refactoring.com/catalog

 
 
 
 

 

댓글