본 글은 리팩터링 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;
}
개선 효과:
함수를 왜 추출해야할까?
가장 중요한 것은 코드가 수행하는 역할을 빠르게 파악하기 위해서다.
하나의 함수가 여러가지 일을 하고 있다면, 코드를 수정해야할 때 어떤 부분이 내가 수정해야할 코드인지 매번 분석해야 한다. 하지만 기능단위로 함수를 추출한다면 해당 함수를 기준으로 수정해야할 영역을 빠르게 구분할 수 있게 된다.
두번 째, 단일 책임 함수를 통해 함수를 구분하면 기능을 추가할 영역을 찾기 쉬우며 이는 개발을 빠르게 진행 할 수 있도록 해준다.
세번 째, 메인 함수를 수정하는 것이 아니라 서브 함수를 수정함으로써 안정성을 확보할 수 있다. 예를들어 유틸함수를 통해 외부에서 주입하는 형태를 떠올리면 함수를 내부적으로 수정하더라도 같은 타입의 값을 반환하면 외부에서는 문제가 발생하지 않는다. 즉 책임이 완전히 분리되어 에러가 발생하는 범위로 서브 함수로 좁혀진다.
마지막으로, 추상화 및 캡슐화로 함수 사용자는 내부 로직을 알 필요없이 입력값과 반환 값의 연관 관계만 파악하면 쉽게 함수를 사용할 수 있다.
이제 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번과 1번 문제는 plays[perf.playID]로 해결이 된다. 그렇다면 함수화의 장점은 무엇일까? 추측해보자.
나름 생각해본 이유는 다음과 같다.
이제 함수의 위의 원리를 적용하기 위해 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);
}
개선 효과:
기껏 정리했지만 아직까지 이 기법을 언제 어디서 적용해야 할지 감이 오지 않는다. 실제 코드에 위 기법을 적용해 보고 기준을 세워보자.