리팩터링 2판 1장 statement 중

Yeongjong Kim·2022년 1월 2일
0

본 글은 리팩터링 2판을 읽으며 생객했던 과정을 기록한 것입니다.

원본 코드

// statement()

function statement(invoice, plays) {
  /// ... 코드 생략
  for (let perf of invoice.performances) {
    const play = plays[perf.playID]; // 이 곳
    let thisAmount = amountFor(perf, play); // 1
    
	switch (play.type) { // 2
      case "tragedy":
        thisAmount = 40000;
        if (perf.audience > 30) {
          thisAmount += 1000 * (perf.audience - 30);
        }
        break;
      case "comedy":
        thisAmount = 30000;
        if (perf.audience > 20) {
          thisAmount += 10000 + 500 * (perf.audience - 20);
        }
        break;
      default:
        throw new Error(`알 수 없는 장르: ${play.type}`);
    }
    volumeCredits += Math.max(perf.audience - 30, 0);

    if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5); // 3

    result += ` ${play.name}: ${format(thisAmount / 100)} (${
      perf.audience
    }석)\n`; // 4
    totalAmount += thisAmount;
  }

  result += `총액: ${format(totalAmount / 100)}\n`;
  result += `적립 포인트: ${volumeCredits}점\n`;
  return result;
};

위의 statement() 코드 중 play = plays[perf.playID]에 주목해보자. 이 코드는 왜 있을까? 당연 plays[perf.playID] 연산을 1번으로 최소화하기 위해서다. play는 위 코드에서 4번 쓰이고 있다. 거의 전반적인 코드에 play가 쓰이며 코드의 흐름 상 상단에 play로 변수화 하는 것은 매우 자연스럽다.

가장 흔히 하는 함수 추출하기(기능단위 분리) 리팩터링 기법을 statement 함수에 적용하는 경우 어떤 일이 발생할까?

가장 중요하다고 판단이 되는 switch문을 분리해보자. switch문은 play의 type(연극의 종류)과 perf의 audience(청중 수)에 따라 thisAmount(가격)을 산정한다. 즉 play 객체와 perf 객체가 반드시 필요한 코드다.

이 두 객체를 매개변수로 받아서 그대로 switch 문을 옮겨보자.

함수 추출하기

function amountFor(perf, play) {
  let thisAmount = 0;

  switch (play.type) {
    case "tragedy":
      thisAmount += 40000;
      if (aPerformance.audience > 30) {
        thisAmount += 1000 * (aPerformance.audience - 30);
      }
      break;
    case "comedy":
      thisAmount += 30000;
      if (aPerformance.audience > 20) {
        thisAmount += 10000 + 500 * (aerformance.audience - 20);
      }
      break;
    default:
      throw new Error(`알 수 없는 장르: ${play.type}`);
  }

  return thisAmount;
}

개선 효과:

  1. for문의 코드량 감소: 페이지 최 상단의 코드와 비교했을 때 for문 코드라인은 32에서 16으로 1/2 만큼 줄었다. 메인 코드를 해석하기 쉬워진다.
  2. 함수 이름을 통해 switch문의 쓰임을 유추 가능: 매번 switch 문이 어떤 코드인지 해석할 필요가 없어진다.

함수를 추출해야하는 이유?

함수를 왜 추출해야할까?

  1. 가장 중요한 것은 코드가 수행하는 역할을 빠르게 파악하기 위해서다.
    하나의 함수가 여러가지 일을 하고 있다면, 코드를 수정해야할 때 어떤 부분이 내가 수정해야할 코드인지 매번 분석해야 한다. 하지만 기능단위로 함수를 추출한다면 해당 함수를 기준으로 수정해야할 영역을 빠르게 구분할 수 있게 된다.

  2. 두번 째, 단일 책임 함수를 통해 함수를 구분하면 기능을 추가할 영역을 찾기 쉬우며 이는 개발을 빠르게 진행 할 수 있도록 해준다.

  3. 세번 째, 메인 함수를 수정하는 것이 아니라 서브 함수를 수정함으로써 안정성을 확보할 수 있다. 예를들어 유틸함수를 통해 외부에서 주입하는 형태를 떠올리면 함수를 내부적으로 수정하더라도 같은 타입의 값을 반환하면 외부에서는 문제가 발생하지 않는다. 즉 책임이 완전히 분리되어 에러가 발생하는 범위로 서브 함수로 좁혀진다.

  4. 마지막으로, 추상화 및 캡슐화로 함수 사용자는 내부 로직을 알 필요없이 입력값과 반환 값의 연관 관계만 파악하면 쉽게 함수를 사용할 수 있다.

함수 사용성 개선하기

이제 amountFor의 사용성을 개선해 보자. 첫 번째로 필요없는 매개변수가 있을까? 매개변수 perf를 지우면 어떤일이 발생할까?

perf는 invoice.performances를 순회하여 얻은 값이다. 즉, perf를 매개변수로 넘기지 않았을 때 접근할 수 있는 방법은 perf를 스코프체이닝에 의해 탐색하는 방법 뿐이다. 하지만 amountFor 함수는 perf의 유효 범위인 for문 블록스코프 외부에 정의됐기 때문에 접근이 불가능하다. 즉 함수를 for문 내부에서 정의해야 하는데, 이는 순회할 때마다 정의하는 코드가 실행되며 효율적이지 못하다. 즉 perf는 무조건 필요한 매개변수다.

그렇다면 play는 어떨까? play의 특성을 생각해보자. play는 plays의 한 프로퍼티 값이다. 즉 plays와 프로퍼티 키를 알고 있다면 접근이 가능한데, perf.playID가 키에 해당하기 때문에 plays만 접근할 수 있다면 매개변수를 제거해도 된다.

여기서도 스코프 체이닝에 의해 plays에 접근해야 하는데, 본인은 함수가 실행될 때 스코프 체이닝을 통해 변수에 접근하는 방식을 별로 좋아하지 않았다. 이유는 외부 변수에 의존하므로써 처해질 위험성이 걱정되었기 때문이다. 혹시모를 사이드 이펙트가 생길 수 도 있고, 변수가 사라질 수도 있다. 이 책에서 그 편견을 깨주었는데, 때에 따라서는 스코프 체이닝을 활용하는게 좋을 수 도 있겠다는 생각을 심어 주었다. (하지만 여전히, 이 방법에 익숙하지 않다.)

plays는 statement 내에서는 어디서든지 접근할 수 있기 때문에 amountFor함수가 statement의 중첩함수이므로 접근이 가능한 범위내에 있다.

가장 쉬운 방법은 amountFor 내에 play를 plays[perf.playID]로 변경하는 것이다. 이 방법을 사용하면 간단하게 문제가 해결된다. 하지만 책에서는 프로퍼티에 접근하는 한줄의 코드를 함수화시켰다.

함수를 추출할 때(amountFor)의 변수들을 각각의 함수로 만들면 수월하다는 내용이 있다. 그 이유는 다음과 같다.

  1. 추출한 함수에 변수를 따로 전달할 필요가 없어진다.
  2. 추출한 함수와 원래 함수의 경계가 더 분명해지기도 한다.(부자연스러운 의존 관계나 부수효과를 찾고 제거하는데 도움이 된다.)

사실 생각해보면 위의 1번과 1번 문제는 plays[perf.playID]로 해결이 된다. 그렇다면 함수화의 장점은 무엇일까? 추측해보자.

나름 생각해본 이유는 다음과 같다.

  1. 예시처럼 변수에 할당하기 위한 코드가 한줄이 아니라 여러줄이 될 경우 함수화는 필수다.
  2. 스코프체이닝에 대한 접근 안정성. plays[perf.playID]의 경우 plays가 어디서 왔고 현재 스코프에서 접근이 가능한지 사용할 때마다 확인해야 한다. 위의 예시에서는 statement 내에서 play에 접근할 수 있기 때문에 그럴 일이 없지만 만약 for문의 perf처럼 특정 스코프 내에서만 접근이 가능한 경우가 예시라면 이를 확인해야 한다. 하지만 확실하게 접근이 가능한 함수를 만들어 놓는다면 비교적 변수를 확인하는 것보다 함수 사용 범위를 확인하는 것이 수월하겠다는 생각이 들었다.
  3. 2번과 일맥상 통하지만 함수를 만들기 때문에 함수의 동작만 확인한다면 안정성을 확보할 수 있다.

play 변수 제거하기

이제 함수의 위의 원리를 적용하기 위해 play 변수를 제거해 보자.
적용된 기법은 임시 변수 play를 질의 함수로 바꾸었고, 질의 함수를 인라인으로 호출했으며 그에 따라 필요없어진 매개변수를 제거함으로써 함수 선언도 바꾸어 주었다.

이하는 최종 코드다.

... in statement()

function playFor(aPerformance) {
  return plays[aPerformance.playID];
}

function amountFor(perf) {
  let thisAmount = 0;

  switch (playFor(perf.playID).type) {
    case "tragedy":
      thisAmount += 40000;
      if (aPerformance.audience > 30) {
        thisAmount += 1000 * (aPerformance.audience - 30);
      }
      break;
    case "comedy":
      thisAmount += 30000;
      if (aPerformance.audience > 20) {
        thisAmount += 10000 + 500 * (aerformance.audience - 20);
      }
      break;
    default:
      throw new Error(`알 수 없는 장르: ${playFor(perf.playID).type}`);
  }

  return thisAmount;
}

for (let perf of invoice.performances) {
  volumeCredits += Math.max(perf.audience - 30, 0);

  if ("comedy" === playFor(perf).type)
    volumeCredits += Math.floor(perf.audience / 5);

  result += ` ${playFor(perf).name}: ${format(thisAmount / 100)} (${
    perf.audience
  }석)\n`;
  totalAmount += amountFor(perf);
}

개선 효과:

  1. 함수의 매개변수가 줄어들며 사용성이 개선됨
  2. statement의 메인 코드인 for문을 보면 지역변수가 사라지며 한결 가독성이 높아짐
  3. 지역변수가 사라지며 신경써야할 유효범위 대상(변수)가 줄어듬

결론

기껏 정리했지만 아직까지 이 기법을 언제 어디서 적용해야 할지 감이 오지 않는다. 실제 코드에 위 기법을 적용해 보고 기준을 세워보자.

빈약하지만 정리하며 확실하게 알게된 것

  1. 임시변수는 스코프 관점에서 문제가 될 수 있다. 따라서 제거할 수 있는지 확인해 보자.
  2. 위 예시처럼 안정성이 확보된 경우에, 스코프 체이닝을 통한 변수 접근을 고려해보자. 그리고 리펙터링으로 코드를 개선해보자.
profile
Front 💔 End

0개의 댓글