* 마틴 파울러님의 Refactoring, Chapter 별로 내용을 다룹니다. Chapter1 리팩터링: 첫 번째 예시 의 관한 글입니다.
POINT. 하나의 기능이 하나의 함수에서 시작되어 용도에 따라 여러개의 함수로 나뉘어가는 흐름을 기억하며, 해당 글을 읽으시면 조금이나마 도움이 될 것입니다.
관련 소스는 github.com/SangchoKim/refactoring/tree/refactoringFirst 에 있습니다.
#1 자, 시작해보자.
"리팩터링에 대한 이야기를 어떻게 시작할까?" 라는 고민으로 책의 서두를 채웁니다. 역사, 원칙보다는 예시용 프로그램을 첫 번째 챕터에서 보여주고, 관련된 상세한 부분들은 추후에 나오는 챕터들에서 보충하는 방식으로 진행이 됩니다. 오히려 지루한 이론보다는 실전에서 사용되는 코드들을 예시로 들어 직관적으로 이해를 할 수 있도록 하는 부분이 가장 마음에 들었습니다. 리팩터링에 대한 이해를 돕기 위해 예시로 만드는 프로그램은 다양한 연극을 외주로 받아서 공여하는 극단이 비용을 측정하는 프로그램입니다.
가상의 극단의 세부적인 사항은 다음과 같습니다.
1. 공연 요청이 들어오면 연극의 장르와 관객 규모를 기초로 입장료을 책정
2. 공연료를 할인 받을 수 있는 포인트 지급
각 파일에 대한 내용은 다음과 같습니다.
1. plays.json - 연극정보
2. invoices.json - 공연료 청구서
3. main.js - 공연료 청구서 출력
----- play.json
{
"hamlet": { "name": "Hamlet", "type": "tragedy" },
"as-like": { "name": "As You Like it", "type": "comedy" },
"othello": { "name": "Othello", "type": "tragedy" }
}
----- invoices.json
{
"customer": "BigCo",
"performances": [
{
"playID": "hamlet",
"audience": 55
},
{
"playID": "as-like",
"audience": 35
},
{
"playID": "othello",
"audience": 40
}
]
}
----- main.js (statement 함수)
import playData from "./plays";
import invoiceData from "./invoices";
const statement = (invoice, plays) => {
let totalAmount = 0;
let volumeCredits = 0;
let result = `청구 내역 (고객명: ${invoice.customer})\n`;
const format = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
}).format;
for (const perf of invoice.performances) {
const play = plays[perf.playID];
let thisAmount = 0;
switch (play.type) {
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);
// 희극 관객 5명마다 추가 포인트 제공
if ("comedy" === play.type) {
volumeCredits += Math.floor(perf.audience / 5);
}
// 청구 내역 출력
result += ` ${play.name}: ${format(thisAmount / 100)} (${
perf.audience
}석)\n`;
totalAmount += thisAmount;
}
result += `총액: ${format(totalAmount / 100)}\n`;
result += `적립 포인트: ${volumeCredits}점\n`;
return result;
};
console.log(statement(invoiceData, playData));
----- 결과
청구 내역 (고객명: BigCo)
Hamlet: $650.00 (55석)
As You Like it: $475.00 (35석)
Othello: $500.00 (40석)
총액: $1,625.00
적립 포인트: 47점
#2 예시 프로그램을 본 소감
프로그램이 간단하기 때문에 이 상태로도 그럭저럭 쓸만하지만 "만약 이런 코드가 수백, 수천 줄짜리의 프로그램이라면 어떻게 될까?" 간단한 인라인 함수라도 이해하기 어려울 것입니다. 책에서는 이렇게 말합니다.
프로그램의 작동방식을 더 쉽게 파악할 수 있도록 코드를 여러 함수와 프로그램 요소로 재구성한다.
만약 프로그램이 새로운 기능을 추가하기에 편한 구조가 아니라면, 먼저 기능을 추가하기 쉬운 형태로 리팩터링하고 나서 원하는 기능을 추가한다.
"기능을 추가하기에 편한 구조"로 리팩터링하는 것이 이 두번째 챕터에서 요구하는 핵심이라고 생각합니다. 현재는 희극과 비극, 이렇게 2개의 장르만 존재하지만, "사극, 고전극, 자유극 등 더 많은 장르가 추가된다면", "추가가 되었을 때 공연료와 포인트 계산법이 바뀌어야 한다면" 이라는 물음표를 떠안고 코드를 구조화해야합니다.
수정 또는 추가가 일어났을 때 statement() 함수가 수정될 것이고, 함수가 커지면서 추가해야 하는 부분들도 늘어남에 따라 함수를 복사했을 때 일관성을 반영하기가 어려워집니다.
"리팩터링은 이러한 변경을 커버하고, 수정과정을 최소화하기 위함입니다."
#3 리팩터링의 첫 단계
리팩터링의 첫 단계는 "코드영역을 꼼꼼하게 검사해줄 테스트 코드" 를 만드는 것 입니다. 위에서 작성한 statement() 함수의 테스트는 어떻게 구성하면 좋을까요? 해당 테스트다음과 같은 순서로 진행 할 수 있습니다.
1. 다양한 장르의 공연들로 구성된 공연료 청구서 여러 개 문자 열로 준비
2. statement() 함수가 반환하는 문자열과 준비해둔 정답 문자열 비교
3. 테스트 결과 보고 (테스트 통과 > 초록불, 테스트 실패 > 빨간불)
4. 반복되는 테스트를 위해서 테스트 프레임워크를 이용한 실행 설정
1-4번 순서로 리팩터링을 할 때마다 자가 테스트를 실행해야 합니다. 테스트를 작성하는 데 시간은 좀 걸리겠지만, 잘 만들어둔 테스트 코드는 전체 작업 시간을 오히려 단축시킬 수 있습니다.
#4 statement() 함수 쪼개기
위에서 만들었던 statement() 함수는 "기능 추가 및 수정"을 고려하여 리팩터링하는 것이 추후 효율적인 코드를 작성하는 데 도움이 될 수 있습니다. 기다란 함수를 리팩터링 할 때는 전체 동작을 각각의 부분으로 나눌 수 있는 지점을 찾는 것이 중요합니다. 여기서는 switch문을 가장 먼저 찾을 수 있었습니다.
코드 내 switch문의 역할은 한 번의 공연에 대한 요금을 계산합니다. 현재는 비극, 희극 2가지 장르로 구성되어 있지만, 더 다양한 장르가 추가되었을 때 코드가 길어진다는 단점과 그로 인해 생겨날 불편한 사항들을 사전에 차단하기 위해 위에 코드 조각을 별도 함수 amountFor()로 추출하는 작업을 시작합니다. 이러한 행위를 "함수 추출하기"라고 합니다.
함수를 추출할 때에는 확인해야 할 사항이 있습니다. 이는 다음과 같습니다.
"유효범위를 벗어나는 함수, 새로운 함수내에서 곧바로 사용할 수 없는 변수가 있는지 확인"
statement() 함수에서는 perf, play, thisAmount가 여기에 속합니다. perf, play는 값이 변경되지 않기 때문에 새로운 함수 매개변수로 전달하면 되지만, thisAmount는 함수 안에서 값이 바뀌므로, 조심해서 변수를 다뤄야 합니다. 새로운 함수에서는 thisAmount 값을 return하도록 작성하였습니다.
이제 추출한 amountFor() 함수를 statement() 함수에서 호출하여 thisAmount 값을 채웁니다.
항상 수정을 하고 난 후에는 테스트를 돌려 실수한 게 없는지 확인하는 습관이 들이는 것이 중요합니다. 또한 리팩터링은 작은 단계로 나눠 진행을 해야 중간에 실수를 하더라도 버그를 쉽게 찾을 수 있기 때문에 피드백 주기를 짧게 가져가는 것이 중요합니다.
자바스크립트로 만들어진 코드이므로 amountFor()를 statement()의 중첩 함수로 만들 수 있습니다. 새로 추출한 함수를 중첩 함수로 만들게 되면 매개변수로 전달 할 필요가 없어 편한 장점이 존재합니다.
새로운 함수를 만들어 코드를 추출한 후에는 네이밍에 대한 생각을 해야합니다. "명확하게 표현할 수 있는 간단한 방법을 찾는 것" 을 책에서는 추천하고 있습니다. 예를 들면 함수의 반환 값에는 "result"라는 변수를 사용하는 것, 매개변수 이름에 접두어로 타입 이름을 적는 것, 매개변수의 역할이 뚜렷하지 않을 때는 부정관사(a, an)를 붙이는 것 등이 이에 포함됩니다.
다음은 매개변수 play를 바꿔야 한다. aPerformance는 반복문을 한 번 돌때마다 자연스럽게 값이 변경되지만, play는 aPerformance에서 얻는 값이기 때문에 애초에 매개변수로 전달할 필요가 없습니다. 임시적으로 들어오는 play 와 같은 매개변수는 코드 복잡도를 높이기 때문에 최대한 제거하는게 좋습니다. 이 책에서는 "임시 변수를 질의 함수로 바꾸기"로 통일하여 부릅니다.
예시 코드에서 해당 순서는 다음과 같습니다.
1. 먼저 대입문(=)의 우변을 함수로 추출
2. 테스트 코드 사용하여 테스트
3. 변수를 인라인으로 바꾸기
이제는 새로 만든 playFor() 함수를 이용하여 본래 하려던 play 매개변수를 제거하고 변수를 인라인으로 바꿀 수 있는 지 확인을 해야 합니다.
순서는 다음과 같습니다.
1. amountFor() 함수의 play 매개변수
2. amountFor() 함수를 호출할 때 play 매개변수
3. 변수를 인라인으로 바꾸기
다음은 "포인트를 적립하는 기능"이 있는 부분을 새로운 함수로 추출하여 보다 명료한 코드로 만들어 보도록 하겠습니다.
추출한 함수에서는 포인트 적립 변수 volumeCredits 의 값을 초기화한 뒤 계산 결과를 반환하도록 하겠습니다.
지금까지 리팩터링한 전체 코드는 다음과 같습니다. (아직 다 끝난게 아닙니다. 리팩터링을 해야 할 부분은 아직 남아있습니다.)
* 3가지 함수가 statement() 함수의 중첩함수로 분리되었습니다.
1. 연극 데이터 추출 함수
2. 각각의 연극 공연료 계산 함수
3. 포인트 적립 함수
import playData from "./plays";
import invoiceData from "./invoices";
const statement = (invoice, plays) => {
let totalAmount = 0;
let volumeCredits = 0;
let result = `청구 내역 (고객명: ${invoice.customer})\n`;
const format = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
}).format;
// 연극 데이터 추출 함수
const playFor = (aPerformance) => {
return plays[aPerformance.playID];
};
// 각각의 연극 공연료 계산 함수
const amountFor = (aPerformance) => {
let result = 0;
switch (playFor(aPerformance).type) {
case "tragedy": // 비극
result = 40000;
if (aPerformance.audience > 30) {
result += 1000 * (aPerformance.audience - 30);
}
break;
case "comedy": // 희극
result = 30000;
if (aPerformance.audience > 20) {
result += 10000 + 500 * (aPerformance.audience - 20);
}
break;
default:
throw new Error(`알 수 없는 장르: ${playFor(aPerformance).type}`);
}
return result;
};
// 포인트 적립 함수
const volumeCreditsFor = (aPerformance) => {
let result = 0;
// 포인트 적립
result += Math.max(aPerformance.audience - 30, 0);
// 희극 관객 5명마다 추가 포인트 제공
if ("comedy" === playFor(aPerformance).type) {
result += Math.floor(aPerformance.audience / 5);
}
return result;
};
for (const perf of invoice.performances) {
volumeCredits += volumeCreditsFor(perf);
// 청구 내역 출력
result += ` ${playFor(perf).name}: ${format(amountFor(perf) / 100)} (${
perf.audience
}석)\n`;
totalAmount += amountFor(perf);
}
result += `총액: ${format(totalAmount / 100)}\n`;
result += `적립 포인트: ${volumeCredits}점\n`;
return result;
};
console.log(statement(invoiceData, playData));
개인적으로 첫 번째 챕터는 이 책에서 가장 중요한 부분이라 생각합니다. 그렇기에 조금 더 자세하게 쓰기 위해 노력하였고, 명료하게 쓰려고 했지만, 양 조절이 조금 힘들었습니다. 그렇기에 첫 번째 챕터는 4개로 나누어 올리도록 하겠습니다.
예시 코드에서 남은 리팩터링은 다음과 같습니다.
1. format 변수 제거하기
2. volumeCredits 변수 제거하기
3. 계산 단계와 포맷팅(html로 표현) 분리하기
4. 다형성을 활용해 계산 코드 재구성하기
다음 글에서는 4가지를 더 나은 문장으로 다루어 보도록 하겠습니다. 긴 글 읽어주셔서 감사합니다.
< 참고자료 >
[책] Refactoring - 마틴 파울러 지음
www.yes24.com/Product/Goods/89649360
<기타> Refactoring (1) end
'책' 카테고리의 다른 글
Refactoring (6) (0) | 2020.12.30 |
---|---|
Refactoring (5) (0) | 2020.12.19 |
Refactoring (4) (0) | 2020.12.15 |
Refactoring (3) (0) | 2020.12.12 |
Refactoring (2) (0) | 2020.12.12 |