리팩터링이라는 책을 읽으려고 하는데, 1장에 간단한 예시와 팁들이 있어서 정리를 하고 가려고 한다. 먼저 예시 코드를 보자.
import plays from "./plays.js"
import invoices from "./invoices.js"
function 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(let 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);
}
thisAmount +=300 * perf.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${play.type}`)
}
volumeCredits +=Math.max(perf.audience -30 ,0);
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(invoices[0],plays))
//plays.js
export default{
"hamlet":{"name": "Halmet", "type":"tragedy"},
"as-like":{"name": "As You Like It", "type":"comedy"},
"othello":{"name":"Othello", "type":"tragedy"}
}
//invoices.js
export default [
{"customer": "BigCo",
"performances":[
{
"playID": "hamlet",
"audience": 55
},
{
"playID":"as-like",
"audience": 35
},
{
"playID":"othello",
"audience":40
}
]}
]
위의 코드는 분명 잘 동작한다. 코드가 지저분하든, 깔끔하든 컴파일러는 그냥 잘 돌릴 거다. 근데 사람은 코드의 미적 상태에 민감하다. 설계가 나쁜 시스템은 수정하기 어렵고, 실수를 저질러서 버스가 생길 가능성도 있다고 한다. 그래서 저자는 수백 줄짜리 코드를 수정할 때면 먼저 프로그램의 작동 방식을 더 쉽게 파악할 수 있도록 코드를 여러 함수와 프로그램 요소로 재구성한다고 한다.
리팩터링의 첫 단계로 테스트 코드들 먼저 작성해야 한다. 테스트는 수시로 해야하고, 반드시 자가진단 하도록 만들어야 한다. 이 테스트에 관한 부분은 4장에서 다룬다고 한다.
그 뒤에 이제 차례로 책의 코드를 따라가며 순차적으로 리팩터링을 진행하는데, 코드의 예시를 다 보여주면 너무 길어지니까 리팩터링 기법들을 간단히 정리하면서 진행하겠다.
먼저 내가 작성한 statement 함수를 쪼개야 한다. 긴 함수를 리팩터링 할 때는 먼저 전체 동작을 각각의 부분으로 나눌 수 있는 지점을 찾는다고 한다. 여기서는 switch 구문이다
또한 여기서 코드를 분석해서 얻은 정보가 있다면 재빨리 코드에 반영해야 한다고 말한다. 그러면 다음번에 코드를 볼 때 분석하지 않다도 된다. 이제 위에서 말한 switch부분을 따로 함수로 만들어 주는데, 이 단계를 함수 추출하기로 이름 붙였다. 먼저 별도 함수로 빼냈을 때 유효범위를 벗어나는 변수, 즉 새 함수에서는 곧바로 사용할 수 없는 변수가 있는지 확인한다. 위의 예시에서는 perf, play, thisAmount가 여기 속한다. perf와 play는 추출한 새 함수에서도 필요하지만 값을 변경하지 않기 때문에 매개변수로 전달하면 된다. 한편 thisAmount는 함수 안에서 값이 바뀌는데, 이런 변수는 조심해서 다뤄야 한다.
function amountFor(perf,play){
let result = 0;
switch(play.type){
case "tragedy":
result = 40000;
if(perf.audience>30){
result+=1000 * (perf.audience -30);
}
break
case "comedy":
result = 30000;
if(perf.audience>20){
result +=10000 +500 * (perf.audience -20);
}
result +=300 * perf.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${play.type}`)
}
return result
}
function 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(let perf of invoice.performances){
const play = plays[perf.playID]
let thisAmount=amountFor(perf,play)
volumeCredits +=Math.max(perf.audience -30 ,0);
if("comedy" ===play.type) volumeCredits +=Math.floor(perf.audience / 5);
result+=`${play.name}: ${format(thisAmount/100)} (${perf.audience} 석) \n`
totalAmount +=thisAmount
}
이렇게 수정한 뒤 곧바로 컴파일 하고 테스트해서 실수한 게 없는지 확인한다.
여기서 '컴파일'이란 자바스크립트를 실행하는 데 필요한 모든 작업을 의미한다. 사실 자바스크립트는 별도의 컴파일 과정 없이 곧바로 실행시킬 수 있기 때문에 컴파일이란 표현이 맞지 않을 수 있지만, 코드를 output 디렉터리로 옮기거나 바벨과 같은 도구를 사용하는 것도 포괄하는 표현으로 사용했다.
뒤에서 따로 언급하지 않겠지만 책에서 리팩토링 단계를 하나씩 실행할 때마다 바로바로 커밋을 해준다. 그래야 중간에 문제가 생기더라도 이전의 정상 상태로 쉽게 돌아갈 수 있다.
다음으로 play 매개변수의 이름을 바꿀 차례다. amountFor 함수를 보면 aPerformance는 루프 변수에서 오기 때문에 반복문을 한 번 돌 때마다 자연스레 값이 변경된다. 하지만 play는 개별 공연에서 얻기 때문에 애초에 매개변수로 전달할 필요가 없다. 그냥 amountFor() 안에서 다시 계산하면 된다. 저자는 긴 함수를 잘게 쪼갤 때마다 play같은 변수를 최대한 제거한 다고 한다. 이런 임시 변수들 때문에 로컬 범위에 존재하는 이름이 늘어나서 추출 작업이 복잡해지기 때문이다. 이를 해결해주는 리팩터링으로는 임시 변수를 질의 함수로 바꾸기가 있다. 또한 위에서 perf로 매개변수를 받았는데 훨씬 알아보기 쉽게 aPerformance로 바꿨다.
function playFor(aPerformance){
return plays[aPerformance.playID]
}
function 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);
}
result +=300 * aPerformance.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${playFor(aPerformance).type}`)
}
return result
}
그다음 단계로는 변수 인라인 하기를 적용해서 statement에서 함수 호출을 통해 변수를 인라인 해준다.
function 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(let perf of invoice.performances){
const play = plays[perf.playID]
let thisAmount=amountFor(perf,playFor(perf))
volumeCredits +=Math.max(playFor(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 +=thisAmount
}
그 뒤 amountFor함수에서도 playFor() 사용해서 play 매개변수를 지워준다.
function 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);
}
result +=300 * aPerformance.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${playFor(aPerformance).type}`)
}
return result
}
지역 변수를 제거해서 얻는 가장 큰 장점은 추출 작업이 훨씬 쉬워진다는 것이다. 저자는 추출 작업 전에는 거의 항상 지역 변수부터 제거한다고 한다.
다음으로는 thisAmount 값이 다시 바뀌지 않기 때문에 thisAmount를 변수인라인하기를 적용해 준다.
이제 다음으로 적립 포인트 계산 코드를 추출해 준다. 결국 이 단계는 statement의 volumCredits를 지워주기 위해 필요하다. 이다음의 과정들은 위에서 한 리팩터링의 반복이기 때문에 과정만 정리를 하겠다.
일단 format함수 의 경우도 임시 변수로 사용 중이어서 함수로 추출해서 써준다. 그리고 위에서 언급한 volumeCredits와 amountFor을 통해 totalAmount를 계산하는 부분을 두 가지 반복문으로 따로 빼준다. 이때 volumeCredits를 계산할 때 문장 슬라이드를 통해 해당되는 반복문 위에 위치시킨다.
function statement(invoice, plays){
let totalAmount =0
let result =`청구 내역 (고객명: ${invoice.customer})\n`;
for(let perf of invoice.performances){
result+=`${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} 석) \n`
totalAmount +=amountFor(perf)
}
let volumeCredits = 0
for(let perf of invoice.performances){
volumeCredits +=volumeCreditsFor(perf)
}
result+=`총액: ${usd(totalAmount)}\n`
result+=`적립 포인트: ${volumeCredits}점 \n`
return result;
}
코드가 이런 식으로 되는데 우리는 최종적으로 volumeCredits와 totalAmount 변수도 제거할 것 이기 때문에 우리가 해왔던 리팩터링이 반복된다. 생략된 게 많지만 다시 한번 정리하면
- 반복문 쪼개기 했고
- 문장 슬라이드 하기 했고
- 함수 추출하기
- 변수 인라인 하기 여기서 변수가 제거됨
이렇게 임시 변수를 질의 함수로 바꾸는 과정을 끝내면 최종적으로 다음과 같다
import plays from "./plays.js"
import invoices from "./invoices.js"
function statement(invoice, plays){
let result =`청구 내역 (고객명: ${invoice.customer})\n`;
for(let perf of invoice.performances){
result+=`${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} 석) \n`
}
result+=`총액: ${usd(totalAmount())}\n`
result+=`적립 포인트: ${totalVolumeCredits()}점 \n`
return result;
}
function totalAmount(){
let result =0
for(let perf of invoices[0].performances){
result+=amountFor(perf)
}
return result
}
function totalVolumeCredits(){
let result=0;
for(let perf of invoices[0].performances){
result +=volumeCreditsFor(perf)
}
return result
}
function usd(aNumber){
return new Intl.NumberFormat('en-US', {style: "currency", currency: "USD", minimumFractionDigits: 2}).format(aNumber/100)
}
function volumeCreditsFor(aPerformance){
let result=0
result +=Math.max(aPerformance.audience -30 ,0);
if("comedy" ===playFor(aPerformance).type)
result +=Math.floor(aPerformance.audience / 5);
return result
}
function playFor(aPerformance){
return plays[aPerformance.playID]
}
function 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);
}
result +=300 * aPerformance.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${playFor(aPerformance).type}`)
}
return result
}
console.log(statement(invoices[0],plays))
중간에 함수들의 리턴 값을 보기 좋게 변수명을 설정해 주었다. 이런 건 기본적으로 계속해줘야 한다. 또한 커밋도 단계가 끝날 때마다 해준다.
위에서 같은 반복문이 쪼개져서 성능이 느려지지 않을까 하는 걱정이 있을 수 있다. 하지만 이 정도 중복은 성능에 미치는 영향이 미미할 때가 많고 실제로 이 코드에서는 차이가 없다. 또한 컴파일러들이 최신 캐싱 기법 등으로 무장하고 있어서 우리의 직관을 초월한 결과를 내어준다. 때로는 리팩터링이 성능에 상당한 영향을 주기도 한다. 그런 경우라도 저자는 개의치 않고 리팩터링 한다고 한다. 잘 다듬어진 코드 라야 성능 개선 작업도 훨씬 수월하기 때문이라고 말한다. 결과적으로 더 빠르게 성능을 개선시킬 수 있다고 한다.
여기까지 기존의 코드와 비교하면 많이 바뀐 게 보인다. 여기까지는 단순히 구조를 보강하는데 주안점을 두었고 다음에는 책에서 원하던 기능 변경 위주로 정리를 할 차례다.
참고 및 출처: <리팩터링 2판> (마틴 파울러 지음, 개앞맵시, 남기혁옮김, 한빛미디어, 2020)
'TIL' 카테고리의 다른 글
Refactoring. 리팩터링 원칙 (0) | 2022.02.04 |
---|---|
Refactoring. 계산 단계와 포맷팅 분리, 다형성 활용한 코드 재구성 (0) | 2022.02.02 |
This Month. 1월 (0) | 2022.01.31 |
This Week. 24 ~ 29 한 주간 정리 (0) | 2022.01.31 |
TS. 타입스크립트 설정 관련 그리고 코드 생성과의 관계 (0) | 2022.01.28 |