[리펙터링 2판] 10장 - 조건부 로직 간소화
도서

[리펙터링 2판] 10장 - 조건부 로직 간소화

반응형

  조건부 로직은 프로그램의 힘을 강화하는 데 크게 기여하지만, 프로그램을 복잡하게 만드는 주요 원인이기도 하다.

 

복잡한 조건문에는 조건문 분해하기(10.1절)

논리적 조합을 명확하게 다듬는 데는 중복 조건식 통합하기(10.2절)

함수의 핵심 로직에 본격적으로 들어가기 앞서 무언가를 검사할때는 (10.3절)

똑같은 분기 로직 (switch문)이 여러곳에 등장하면 (10.4절)

널과 같은 특이 케이스를 처리하는데도 조건부 로직이 흔히 쓰이고 거의 똑같으면 (10.5절)

프로그램의 상태를 확인하고 그결과에 따라 다르게 동작해야하는 상황이면 (10.6 절)

 

 

10.1 조건문 분해하기

내용

  • 다양한 조건, 그에따라 동작도 다양한 코드를 작성하면 순식간에 긴 함수가 된다.
  • 긴 함수는 그자체로 읽기 어렵지만, 조건문은 그 어려움을 한층 가중시킨다.

적용

  • 코드를 부위별로 분해한 다음 해체된 코드덩어리들을 각 덩어리의 의도를 살린 이름의 함수로 호출

결과

  • 해당 조건이 무엇인지 강조하고, 무엇을 분기했는지 명백해짐. 
  • 분기 이유 역시 명확해짐

예시

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

 

  조건문이 길어져 리팩터링을 하지 않으면, 조건을 검사하고 그 결과에 따른 동작을 표현한 코드는 무슨 일이 일어나는지 이야기해주지만 '왜' 일어나는지 말해주지 않을 때가 문제가 된다.

if ~ else 문을 사용할 때, 3항 연산자로 바꿔주는 것도 좋은 방법이다.

 

10.2 조건식 통합하기

내용

  • 비교하는 조건은 다르지만 그 결과로 수행하는 동작이 똑같은 코드가 있으면, 조건 검사도 하나로 통합하는게 낫다.

적용

  • 조건식을 통합해야하는 이유는 이 리팩터링을 하지 말아야 하는 이유도 설명해준다.
  • 하나의 검사라고 생각 할 수 없는, 독립된 검사들이라 판단되면 이 리팩터링을 해서는 안된다.

결과

  • 코드의 의도가 더 명확해질 때도 있다.
  • 함수 추출하기(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));
  }

 

하나의 함수 안에서 같은 위치에서 if문이 반복되고 있다면 or 연산자를 이용하여 하나의 조건식으로 변경 가능하다.

if 조건문 안에서 if문이 반복되었을 경우 and 연산자를 이용하여 하나의 조건식으로 변경 가능하다.

두 경우가 복합된 상황에서는 and와 or을 적절히 섞어 사용하고, 복잡한 상황일 경우가 높아 코드가 지저분하다. 그러므로 함수 추출하기를 적절히 활용하여 전체를 더 이해하기 쉽게 만들어 주면 좋다.

 

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

내용

  • 함수에서 조건문을 사용할 때, 거짓인(비정상)경우를 조건식에 대입한다.
  • 이 조건식에서 비정상 경우가 참일 때(비정상이 맞다면), 함수에서 빠져나오는 검사형태를 보호구문이라고 한다.

적용

  • 비정상인 경우(거짓)를 가려낼 경우에 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 구문을 사용하는 것이다.

하지만 거기에 썩어있는 사과가 있거나, 돌맹이가 있는 경우에 else를 복잡하게 사용할 필요 없이 if문을 사용하여 제거하는 것이 좋다.

정리하자면 원치 않은 결과는 else를 사용하지 않고 if문으로 곧바로 나눠주는게 좋다.

 

 

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

내용

  • 복잡한 조건부 로직은 프로그래밍에서 해석하기 난해하지만, 클래스와 다형성을 이용하면 더 확실하게 분리 된다.
  • 다형성은 객체 지향 프로그래밍의 핵심이지만 모든 조건부 로직을 다형성으로 대체해야 하는 견해에는 동의 하지 않는다.

적용

  • switch문이 포함된 함수가 여러개 보일 경우, case별로 클래스를 하나씩 만들어 공통 switch 로직의 중복을 없앨 수 있다.
  • 다른 방법으로는 로직을 슈퍼클래스에 넣어 변형 동작에 신경쓰지 않게 만들고, 변형 동작을 뜻하는 case들을 각각의 서브클래스로 만든다.

결과

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

 

  글쓴이가 이야기 하고자 하는 것은 조건부 로직을 직관적으로 구조화할 방법을 고민하고, '복잡한 조건부 로직'을 발견하면 다형성이 막강한 도구임을 깨닫게 된다가 핵심이다. 그리고 모든 조건부 로직을 다형성으로 대체하는 것에 옳지 않다고 이야기하고 있다.

내 생각도 그러한 것이, 위 예시는 복잡하지않는 기본적인 책에 있는 예시이고 간단해서 switch 문이나 class 문이나 별반 다를게 없다 생각이 들었다. 하지만 복잡성이 증가한다고 상상해 보았을 때 어느 것이 더 직관적일까에 대해서는 당연히 후자일게 분명하다.

 

 

10.5 특이 케이스 추가하기

내용

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

적용

  • 단순히 데이터를 읽기만 한다면 반환한 값들을 담은 리터럴 객체 형태
  • 그 이상의 어떤 동작을 수행해야 한다면 필요한 메서드를 담은 객체를 생성
  • 캡슐화한 클래스가 반환하거나 변환을 거쳐 데이터 구조에 추가시키는 형태가 될 수도 있다.

결과

  • 특이 케이스를 확인하고 처리하는 코드를 모아 효율적으로 관리

예시

Before

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

After

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

 

 

10.5 특이 케이스 추가하기

내용

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

적용

  • 단순히 데이터를 읽기만 한다면 반환한 값들을 담은 리터럴 객체 형태
  • 그 이상의 어떤 동작을 수행해야 한다면 필요한 메서드를 담은 객체를 생성
  • 캡슐화한 클래스가 반환하거나 변환을 거쳐 데이터 구조에 추가시키는 형태가 될 수도 있다.

결과

  • 특이 케이스를 확인하고 처리하는 코드를 모아 효율적으로 관리

예시

Before

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

After

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

 

 

10.6 어서션 추가하기

내용

  • 특정 조건이 참일 때만 제대로 동작하는 코드 영역이 있다 가정을 코드에 항상 명시적으로 기술되어있지 않다.
  • 어서션은 항상 참이라고 가정하는 조건부 문장으로, 어서션이 실패했다는 건 프로그래머가 잘못했다는 뜻이다.

적용

  • 참이라고 가정하는 조건이 보이면 그 조건을 명시하는 어서션을 추가한다.
  • 어서션은 오류찾기에 그치지 않고 어떤 상태임을 가정한 채 실행되는지를 다른 개발자에게 알려주는 훌륭한 소통 도구인 것이다.
  • 테스트 코드가 있다면 어서션의 디버깅 용도로서의 효용은 줄어들지만 소통만큼은 여전히 매력적이다.

결과

  • 버그를 잡을 뿐만 아니라 소통으로써 장점이 있다.

예시

Before

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

After

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

 

  어서션을 남발 하는 것은 위험하다. 어서션은 '반드시 참이어야 하는' 것만 검사한다. 그리고 프로그래머가 일으킬만한 오류에만 어서션을 사용한다. 데이터를 외부에서 읽어 온다면 그 값을 검사하는 작업은 예외 처리로 대응해야 하는 프로그램 로직의 일부로 다뤄야 한다.

어서션은 버그 추적을 돕는 최후의 수단이다. 하지만 글쓴이는 절대 실패하지 않으리라 믿는 곳에만 사용한다.

 

 

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

내용

  • 제어 플래그란 코드의 동작을 변경하는데 사용되는 변수
  • 어딘가에서 값을 계산해 제어 플래그에 설정한 후 다른 어딘가의 조건문에서 검사하는 형태는 악취라 본다.

적용

  • 제어 플래그의 주 서식지는 반복문 안이다.(break문, continue문 활용에 익숙하지 않거나, return문은 하나로 유지하려 할 때)

결과

  • 버그를 잡을 뿐만 아니라 소통으로써 장점이 있다.

예시

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;
  }
반응형