프로그래밍

[리팩토링-1] 리팩토링(2판) - 첫번째 예시(1) ~ 1.4까지

가시가되어 2024. 1. 12. 17:33
오래된 시스템 운영자로서 소스코드를 어떻게 하면 리팩터링 할 수 있을지 에 대해서
생각해보기 위해 읽고 정리

 

먼저 앞 챕터에서 예시를 통한 리팩토링 과정을 설명하고 6~12장에서 기법들에 대해서 설명하는 구조

 

 

역자관련 깃허브 링크

https://github.com/WegraLee/Refactoring

 

GitHub - WegraLee/Refactoring: 『리팩터링, 2판』(한빛미디어, 2020)

『리팩터링, 2판』(한빛미디어, 2020). Contribute to WegraLee/Refactoring development by creating an account on GitHub.

github.com

 

1.3 리팩터링 첫 단계

  • 프로그램이 새로운 기능을 추가하기에 편한 구조가 아니라면, 먼저 기능을 추가하기 쉬운형태로 리팩터링을 하고 나서 원하는 기능을 추가한다.
  • 오래 사용할 프로그램이라면 중복코드는 골칫거리가 된다. - 항시 일관되게 수정했는 지도 확인해야 함. 

  *  예시에서는 statement() 함수라는 기존함수에서 HTML을 출력하는 기능을 추가할 때 기존함수를 카피하여 htmlStatement() 라는 함수를 생성할 경우 발생하는 문제를 고려하고 있음.

// 예시 자바스크립트 : invoice.json, plays.json
// 오타 있을 수 있음.
function statement(invoice, plays) {

	let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : $(invoice.customer})\n`;
    const format = new Intl.NumberFormat("en-US", { style: "currencty", 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;

}

 

 

리팩터링의 첫 단계는 항상 똑같다.

리팩터링할 코드 영역을 꼼꼼하게 검사해줄 테스트 코드가 있어야 한다.

리팩터링 기법들이 버그 발생 여지를 최소화하도록 구성됐다고는 하나, 실제 작업은 사람이 하기 때문에 실수할 수 있다.

 

  • 리팩터링 하기 전에는 제대로 된 테스트부터 마련한다. 테스트는 반드시 자가진단하도록 만든다.
  • 테스트 작성 시간이 걸리지만 잘 만들어두면 디버깅 시간을 줄일 수 있어 전체 작업 시간이 단축된다.

 

1.4 statement () 함수 쪼개기

 

긴 함수(메서드)를 리팩터링할 땐 전체 동작을 각각의 부분으로 나눌 수 있는 지점을 찾아야 한다.

1. 중간 부분의 Swicth 문

  • 한번의 공연에 대한 요금을 계산하고 있다.

       -> 이러한 정보를 얻는 건 코드를 분석해서 얻은 정보이며 이런식으로 파악한 정보는 머리로 저장된 정보이기에 휘발성이 높음, 따라서 바로 코드로 반영을 해야 다음 번에 코드를 볼 때 분석하지 않아도 됨.

 

  • 코드 조각을 별도 함수로 추출하는 방식으로 파악한 정보를 코드에 반영, 추출한 함수에는 그 코드가 하는 일을 설명하는 이름을 지어준다 -> 이 절차를 따로 함수(메서드) 추출하기라는 저자는 이름을 명명하였다
  • 먼저 별도 함수로 빼냈을 때, 유효범위를 벗어나는 변수, 즉 새 함수에서 곧바로 사용할 수 없는 변수가 있는 지 확인
  • 예시에서의 perf와 play는 파라미터로 전달해도 무방(값을 변경하지 않기 때문)
  • 하지만 thisAmount 의 경우, 함수 안에서 값이 변경된다. 이러한 변수는 조심해야 하며 해당 예시에서는 이러한 변수가 홀로 사용되어 해당 값을 리턴하도록 구현 
    function amountFor(perf, play) {   
       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}`);
          }
          
       return thisAmount   
          
   }

 

 

 

amountFor를 통해서 thisAmount 값을 계산하게 변경됨.

// 예시 자바스크립트 : invoice.json, plays.json
// 오타 있을 수 있음.
function statement(invoice, plays) {

	let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : $(invoice.customer})\n`;
    const format = new Intl.NumberFormat("en-US", { style: "currencty", 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;
         
        }
         
         result += `총액: ${format(totalAmount/100)}\n`;
         result += `적립 포인트 : ${volumeCredits}점\n`;
         return result;

}

 

  • 리팩터링은 프로그램 수정을 작든 단계로 나눠 진행한다. 그래서 중간에 실수를 하더라도 버그를 쉽게 찾을 수 있다.

간단한 수정이라도 리팩터링 이후에는 세트스하는 습관을 들여야 한다.

조금씩 변경하고, 매번 테스트하는 것은 리팩터링 절차의 핵심이다.

함수 추출하기의 경우, 흔히 IDE에서 자동으로 수행해준다.

 

IDE를 이클립스에서 함수추출 시, Extract Method (Alt + Shift + M)

    추출할 코드 범위를 선택 후 해당 단축키 혹은 오른쪽 마우스 > Refactor > Extract Method 실행

 

 

함수를 추출하고 나면, 추출된 함수 코드 내에서 지금보다 더 명확하게 표현할 수 있는 간단한 방법이 없는 지 검토한다.

가장 먼저 변수 이름을 더 명확하게 변경하기

(Ex. amountFor 내의 thisAmount 변수를 result 로 변경한다)

* 저자의 경우, 함수의 반환값에는 항상 result로 명명한다.

다음, perf 를 aPerformance 로 리팩터링 (매개변수의 역할이 뚜렷하지 않을 때 부정관사(a/an)을 붙인다.)

 

    function amountFor(aPerformance, play) {   
       let result = 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 * (aPerformance.audience - 20);
                 }
                 thisAmount += 300 * aPerformance.audience;
                 break;
         	default:
            	throw new Error(`알수 없는 장르: ${play.type}`);
          }
          
       return result;   
          
   }

 

 

 

play 변수 제거하기

play 변수의 경우, aPerformance에서 얻을 수 있으므로 매개변수로 전달할 필요가 없다.

이러한 임시변수들을 해결해주는 '임시변수를 질의 함수로 바꾸기' 가 있다.

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

 

 

'변수 인라인하기'  -> '함수 선언 바꾸기'

playFor()를 사용하도록 amountFor() 변경

현재 리팩터링되면서, 이전 코드는 루프를 한 번 돌때마다 공연을 조회했으나 리팩터링 코드에서는 세 번이나 조회

(성능과의 관계에 대해서는 책에서는 뒤에서 후술, 크게 영향은 없으나 설사 느려지더라도 리팩터링된 코드 베이스가 더욱 성능을 개선하기 수월하다.)

 

지역변수를 제거함으로써 추출 작업이 더 쉬워짐

amountFor()는 임시 변수 thisAmount 를 값을 설정하는데 사용되는데 그 값이 변경되지 않음.

따라서 '변수 인라인하기' 를 적용

// 예시 자바스크립트 : invoice.json, plays.json
// 오타 있을 수 있음.
function statement(invoice, plays) {

	let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : $(invoice.customer})\n`;
    const format = new Intl.NumberFormat("en-US", { style: "currencty", currency: "USD", minimumFractionDigits: 2}).format;
    
    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(amountFor(perf)/100)} (${perf.audience} 석) \n `;
        totalAmount += amountFor(perf);
         
        }
         
         result += `총액: ${format(totalAmount/100)}\n`;
         result += `적립 포인트 : ${volumeCredits}점\n`;
         return result;

}

function amountFor(aPerformance) {   
       let result = 0;
       switch (plyerFor(aPerformance).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 * (aPerformance.audience - 20);
                 }
                 thisAmount += 300 * aPerformance.audience;
                 break;
         	default:
            	throw new Error(`알수 없는 장르: ${plyerFor(aPerformance).type}`);
          }
          
       return result;   
          
   }

 

 

 

적립포인트 계산 코드 추출하기

 

volumeCredits는 Loop를 돌 때마다 값을 누적해야 함.

새롭게 추출한 함수에서 복제본을 초기화 한 뒤, 계산 결과를 반환토록 함.

// 예시 자바스크립트 : invoice.json, plays.json
// 오타 있을 수 있음.
function statement(invoice, plays) {

	let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : $(invoice.customer})\n`;
    const format = new Intl.NumberFormat("en-US", { style: "currencty", currency: "USD", minimumFractionDigits: 2}).format;
    
    for(let 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;

}

fucntion volumeCreditsFor(perf) { // 새롭게 추출한 함수
	let volumeCredits = 0;
    volumeCredits += Math.max(perf.audience -30, 0);
    if("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience / 5);  
    
    return volumeCredits;
}

function amountFor(aPerformance) {   
       let result = 0;
       switch (plyerFor(aPerformance).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 * (aPerformance.audience - 20);
                 }
                 thisAmount += 300 * aPerformance.audience;
                 break;
         	default:
            	throw new Error(`알수 없는 장르: ${plyerFor(aPerformance).type}`);
          }
          
       return result;   
          
   }

 

 

format 변수 제거하기

 

format 은 임시변수에 함수를 대입한 형태이다.

이를 함수를 직접 선언하여 변경

function format(aNumber) {
	return new Intl.NumberFormat("en-US", 
    {style: "currency", currency: "USD", minimumFractionDigits: 2}).format(aNumber);
}

 

format 의 이름을 더 명확하게 변경하기

해당 함수의 핵심은 화폐 단위를 맞추는 것이고 해당 기능은 화면에서 출력할 때 달러 단위로 변환하므로 usd() 명명하며

미국에서는 금액을 센트 단위의 정수로 저장한다. 달러미만을 표현할 때, 부동소수점을 사용하지 않아도 되며 산술 연산도 쉽게 처리한다. 화면에 출력할 때는 다시 달러로 표현하므로, 나누는 기능도 포멧함수에서 같이 처리하고 있다.

 

function usd(aNumber) {
	return new Intl.NumberFormat("en-US", 
    {style: "currency", currency: "USD", minimumFractionDigits: 2}).format(aNumber/100); // 나눗셈 단위변환로직도 추가됨.
}

 

 

 

* 마무리

아직 statement () 함수 쪼개기가 다 마무리된 것은 아님

volumeCredits 변수 제거하기

 

느낀점 : 

중간중간 컴파일 - 테스트 - 커밋 과정을 반복

테스트에 대해서 고민해볼 것.

리팩터링을 위한 기법들이 많음.

지역변수의 사용이나 함수 기능을 분리/제거하고 함수(메서드)가 하는 일에 대해서 정확히 표현하는 것(이름 짓기)에 대해 고려해볼 것.

변수 제거 작업 등의 작업의 단계를 아주 세밀하게 나누어 볼 것.