본문 바로가기
핌팩토링

10. 조건문 로직 간소화

by AsCE_hyunseung 2022. 4. 28.

10.1 조건문 분해하기

배경

  • 복잡한 조건부 로직은 프로그램을 복잡하게 만드는 원흉이다.
    • 조건을 검사하고 그 결과에 따른 동작을 표현한 코드는 모슨 일이 일어나는지는 이야기해주지만 ‘왜'일어나는지는 제대로 말해주지 않을 때가 많은 것이 문제다.
  • 거대한 코드 블록이 주어지면 코드를 부위별로 분해한 다음 해체된 코드 덩어리들을 각 덩어리의 의도를 살린 이름의 함수 호출로 바꿔준다.

절차

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

예시

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

------------------------------------------------------
if (summer()) {
    charge = quantity * plan.summerRate;
} else {
    charge = quantity * plan.regularRate + plan.regularServiceCharge;
}

function summer() {
    return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)
}

10.2 조건식 통합하기

배경

  • 어차피 같은 일을 할 거라면 조건 검사도 하나로 통합하는 게 낫다.
  • 조건부 코드를 통합하는 게 중요한 이유는 두 가지가 있다.
    1. 여러 조각으로 나뉜 조건들을 하나로 통합함으로써 내가 하려는 일이 더 명확해진다.
    2. 이 작업이 함수 추출하기까지 이어질 가능성이 높다.
  • 독립적인 검사들이라고 판단되면 이 리팩토링을 행하면 안된다.

절차

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

예시

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

---------------------------------------------

if (isNotEligibleForDisablity()) return 0;

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

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

배경

  • 조건문은 참인 경로와 거짓인 경로 모두 정상 동작으로 이어지는 형태, 한쪽만 정상이 형태 이렇게 두 형태가 있다.
  • 두 형태는 의도하는 바가 서로 다르므로 그 의도가 코드에 드러나야 한다.
    • 두 경로 모두 정상 동작이라면 if-else 절을 사용하고, 한쪽만 정상이라면 비정상 조건을 if에서 검사한다음 참이면 함수에서 빠져나온다. (보호 구문)
  • 이 리팩터링 기법의 핵심은 의도를 부각하는 데 있다.
    • 보호 구문은 “이건 이 함수의 핵심이 아니다. 이 일이 일어나면 무언가 조치를 취한 후 함수에서 빠져나온다" 라고 이야기 한다.

절차

  1. 교체해야 할 조건 중 가장 바깥 것을 선택하여 보호 구문으로 바꾼다.
  2. 모든 보호 구문이 같은 결과를 반환하다면 보호 구문들의 조건식을 통합한다.

예시

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

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

배경

  • 조건문 구조를 클래스와 다형성을 이용해서 분리한다.
  • case별로 클래스를 하나씩 만들어 공통 switch 로직의 중복을 없앨 수 있다.
    • 다형성을 활용하여 어떻게 동작할지를 각 타입이 알아서 처리하도록 하면 된다.

절차, 예시

  1. 다형성 동작을 표현하는 클래스들이 아직 없다면 만들어준다. 인스턴스를 만들어 반환하는 팩터리 함수도 함께 만든다.
  2. 호출하는 코드에서 팩터리 함수를 사용하게 한다.
  3. 조건부 로직 함수를 슈퍼클래스로 옮긴다.
  4. 서브클래스 중 하나를 선택한다. 서브클래스에서 슈퍼클래스의 조건부 로직 메서드를 오버라이드한다. 조건부 문장 중 선택된 서브클래스에 해당하는 조건절을 서브클래스 메서드로 복사한 다음 적절히 수정한다.
  5. 같은 방식으로 각 조건절을 해당 서브클래스에서 메서드로 구현한다.
  6. 슈퍼클래스 메서드에는 기본 동작 부분만 남긴다. 슈퍼클래스가 추상 클래스여야 한다면, 이 메서드를 추상으로 선언하거나 서브클래스에서 처리해야 함을 알리는 에러를 던진다.
switch (bird.type) {
    case '유럽 제비':
        return "보통이다";
    case '아프리카 제비':
        return (bird.numberOfCoconuts > 2) ? '지쳤다' : '보통이다';
    case '노르웨이 파랑 앵무':
        return (bird.voltage > 100) ? '그을렸다.' : '예쁘다';
    default:
        return '알 수 없다';
}
------------------------------------------------------------
class EuropeanSwallow {
    get plumage() {
        return "보통이다";
    }
}
class AfricanSwallow {
    get plumage() {
        return (bird.numberOfCoconuts > 2) ? '지쳤다' : '보통이다';
    }
}
class NorwegianBlueParrot {
    get plumage() {
        return (bird.voltage > 100) ? '그을렸다.' : '예쁘다';
    }
}

10.5 특이 케이스 추가하기

배경

  • 코드베이스에서 특정 값에 대해 똑같이 반응하는 코드가 여러 곳이라면 급 반응들을 한 데로 모으는 게 효율적이다.
  • 특수한 경우의 공통 동작을 요소 하나에 모아서 사용하는 특이 케이스 패턴을 활용하면 특이 케이스를 확인하는 코드 대부분을 단순한 함수 호출로 바꿀 수 있다.

절차, 예시

  1. 데이터 구조에 특이 케이스인지를 검사하는 속성을 추가하고, false를 반환하게 한다.
  2. 특이 케이스 객체를 만든다. 이 객체는 특이 케이스인지를 검사하는 속성만 포함하며, 이 속성은 true를 반환하게 한다.
  3. 클라이언트에서 특이 케이스인지를 검사하는 코드를 함수로 추출한다. 모든 클라이언트가 값을 직접 비교하는 대신 방금 추출한 함수를 사용하도록 고친다.
  4. 코드에 새로운 특이 케이스 대상을 추가한다. 함수의 반환 값으로 받거나 변환 함수를 적용하면 된다.
  5. 특이 케이스를 검사하는 함수 본문을 수정하여 특이 케이스 객체의 속성을 사용하도록 한다.
  6. 여러 함수를 클래스로 묶기나 여러 함수를 변환 함수로 묶기를 적용하여 특이 케이스를 처리하는 공통 동작을 새로운 요소로 옮긴다.
  7. 아직도 특이 케이스 검사 함수를 이용하는 곳이 남아 있다면 검사 함수를 인라인한다.
let weeksDelinquent = isUnknown(customer) ? 0 : customer.paymentHistory.weeksDelinquentInLastYear
--------------------------------------------------
class UnknownCustomer {
    get paymentHistory {
        return new NullPaymentHistory();
    }
}

class NullPaymentHistory {
    get weeksDelinquentInLastYear {
    	return 0; 
    }
}

let weeksDelinquent = customer.paymentHistory.weeksDelinquentInLastYear

10.6 어서션 추가하기

배경

  • 특정 조건이 참일 때만 제대로 동작하는 코드 영역이 있을 수 있다.
  • 어서션을 이용해서 코드 자체에 삽입해놓는 것이다.
  • 어서션의 존재 유무가 프로그램 기능의 정상 동작에 아무런 영향을 주지 않도록 작성돼야 한다.
  • 테스트 코드가 있다면 어서션의 디버깅 용도로서의 효용은 줄어든다.

절차, 예시

  1. 참이라고 가정하는 조건이 보이면 그 조건을 명시하는 어서션을 추가한다.
if (this.discountRate) {
    base -= (this.discountRate * base)
}
----------------------------------------
assert(this.discountRate >= 0);
if (this.discountRate) {
    base -= (this.discountRate * base)
}

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

배경

  • 제어 플래그란 코드의 동작을 변경하는 데 사용되는 변수를 말한다.
  • return 문을 하나로 유지하기보다는 함수에서 할 일을 다 마쳤다면 그 사실을 return 문으로 명확히 하는게 낫다.

절차, 예시

let found = false;
for (const p of people) {
    if (!found) {
        if (p === '조커') {
            sendAlert();
            found = true;
        }
        if (p === '사루만') {
            sendAlert();
            found = true;
        }
    }
}
  1. 제어 플래그를 사용하는 코드를 함수로 추출할지 고려한다.
  2. checkForMiscreants(people); function checkForMiscreants(people) { let found = false; for (const p of people) { if (!found) { if (p === '조커') { sendAlert(); found = true; } if (p === '사루만') { sendAlert(); found = true; } } }
  3. 제어 플래그를 갱신하는 코드 각각을 적절한 제어문으로 바꾼다.
  4. checkForMiscreants(people); function checkForMiscreants(people) { let found = false; for (const p of people) { if (!found) { if (p === '조커') { sendAlert(); **return**; } if (p === '사루만') { sendAlert(); **return**; } } } }
  5. 모두 수정했다면 제어 플래그를 제거한다.

'핌팩토링' 카테고리의 다른 글

12. 상속 다루기  (0) 2022.06.19
11. API 리팩터링  (0) 2022.06.19
9. 데이터 조직화  (0) 2022.04.28
8. 기능 이동  (0) 2022.04.27
7. 캡슐화  (0) 2022.04.27