컴퓨터가 이해하는 코드는 바보도 작성할 수 있다.
사람이 이해하도록 작성하는 프로그래머가 진정한 실력자다.
1. 리팩토링하기전에는 테스트 부터
분량이 방대한 코드를 리팩토링하다보면 잘 돌아가던 코드도 안되는 경우가 빈번하게 생기기 마련이다. 때문에 지속적으로 눈으로 보며 기능이 정상적으로 동작하는지 확인하는 과정이 필요하다. 이를 리팩토링 - 기능검사 순으로 반복적으로 수행하다보면 너무나 비효율적이고 검수에 너무나 많은 시간이 소요될 것이다. 이를 위해서 테스트 작성에 시간이 걸리더라도 신경 써서 만들어두면 디버깅 시간이 줄어 전체 작업시간은 줄어드는 효과를 얻을 수 있다.
2. 기능 쪼개기
긴 함수의 리팩토링을 맞닥뜨렸을 때 당황하지말고 기능쪼개기부터하자. 본문에서 발췌한 다음과 같은 코드를 보자.
function statement(invoice, plays){
let totalAmount = 0;
let volumeCredits = 0;
let result = `청구내역 ${invoice.customer}`;
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':
this.Amount = 30000;
if(perf.audience >20)
this.Amount += 10000 + 500 * (perf.audience -20);
this.Amount += 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}석`;
result += `적립 포인트: ${volumeCredits}점`
return result;
}
}
switch 문에 있는 로직은 지금 당장은 짧아서 무슨 기능을 하는지 명시적으로 알수 있지만, 코드가 길어 질 수록 해당 부분은 휘발성으로 작성자의 머릿속에만 남아있는 로직이 되어 버릴 것이다. 다른 문제점으로는 장르를 추가했을 때 코드를 일일히 뜯어내서 필요한 부분에 로직을 추가해주어야 된다. 좋은 코드는 '누가 보더라도 얼마나 수정하기 쉬운가'에 달려있다고 봐도 무방하다. 그런 관점에서 이 코드는 복잡하고 단번에 알기 어렵다. 누구든지 코드를 봤을 때 디버깅이 쉬워야 한다. 이를위해 코드 자신이 하는일을 추출해서 반영할 필요가 있다. 그리고 perf 같은 알아보기 힘든 변수명도 명시적으로 치환해 주어야 한다.
2-1 임시변수를 질의 함수로 바꾸기 , 변수 인라인하기
위의 코드에서 plays[perf.playId]는 plays에서 가져오기 때문에 굳이 매개변수를 둘 필요가 없다. 이를
function playFor(aPerformance){
return plays[aPerformance.playId];
}
로 추출해주면 const play = playFor(perf)로 표현될 수 있으며 let thisAmount = amountFor(perf, playFor(perf))로 인라인으로 처리해 줄 수도 있다.
2-2 임시 변수는 최대한 제거하라
임시변수는 나중에 문제를 일으킬 수 있다. 임시 변수는 자신이 속한 곳에서만 의미를 가지며 복잡성을 증대시킨다. 때문에 최대한 변수를 인라인하고 함수를 직접 선언해 사용하는 것이 좋다. 예를들어 getter나 computed속성으로 처리되는 부분들같은 경우가 그 예이다.
2-3 반복문 쪼개기
한 반복문안에 성능을 위해 여러 로직을 우겨넣었다면, 이는 동일한 기능을 하는 로직끼리 묶어 반복문을 쪼개주는 것이 좋다. 물론 2개로 나뉘면 성능면에서 걱정할 우려도 있다. 하지만 이는 굉장히 미미하며, 이 부분을 수정한다해도 대부분의 경우는 성능 차이를 체감하기 어렵다. 일단 리팩토링 후 성능 이슈가 생긴다면 그 때 판단해도 오히려 리팩토링 되어 빠르게 디버깅해서 이를 개선할 수 있을 것이다.
2-4 반복문을 파이프라인으로 바꾸기
for문은 최대한 고차함수를 이용해서 치환해준다.
return data.performances.reduce((total,p) => total + p.amount ,0);
3. 변수명 짓기
이 책에 서술된 내용 1장에는 변수명에 다음과 같은 규칙을 준다고 한다.
1. 함수의 반환 값에는 항상 result 를 사용한다.
2. 매개변수의 역할이 뚜렷하지 않을 때에는 부정관사(a/an)을 붙인다.
* 이름이 좋으면 함수 본문을 읽지 않고도 무슨 일을 하는 함수인지 알 수 있다.
4. 단계 쪼개기
위의 경우 statement()에 필요한 데이터를 처리하고 이후 텍스트를 렌더하는 과정을 거친다. 이를 하나의 메소드에 표현하고 있는데, 확실하게 단계적으로 구분 짓는 것이 좋아보인다. 데이터 처리를 1단계, 출력 2단계로 나누어 준다.
function statement(invoice, plays){
const statement = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
return renderPlainText(statementData, plays);
function enrichPerformance(aPerformance){
const result = Object.assign({}, aPerformance);
result.play = playFor(result);
return result;
}
function playFor(aPerformance){
return plays[aPerformance.playId];
}
}
* mutable한 데이터는 금방 상하기 때문에 얕은복사를 통해 최대한 immutable하게 다뤄준다.
5. 다형성을 이용한 코드 재구성 / 생성자를 팩토리 패턴으로 바꾸기
위의 공연료 계산을 performanceCalculater클래스로 코드를 이전하고 공연 종류에 따라 다른 조건부 로직들을 다형성을 활용해 재구성해줄 수 있다.
function createPerformanceCalculator(aPerformance, aPlay){
switch(aPlay.type){
case 'tragedy': return new TragedyCalculator(aPerformance, aPlay);
case 'comedy': return new ComedyCalculator(aPerformance, aPlay);
default: throw new Error();
}
}
class TragedyCalculator extends PerformanceCalculator{}
class ComedyCalculator extends PerformanceCalculator{}
공통 로직은 PerformanceCalculator 슈퍼클래스에 정의하고 오버라이드해 사용해주면 중복코드를 방지할 수 있다. 일반적인 슈퍼셋을 슈퍼클래스에 남겨두고 예외적인 부분을 필요할 때 오버라이드하게 만드는 것이 좋다. 위의 경우 포인트 계산 방식이 조금 다른 코미디 처리 로직을 해당 서브클래스로 내리는 것이 좋다.
2장에서 계속..