Computer기본지식/리팩터링

리팩터링 Chapter 1 - 첫 번째 예시

HOONY_612 2021. 8. 12. 12:41
반응형

이번에 읽고 토론 해 볼 책은 "리팩터링"이라는 책입니다.

이 책은 마틴 파울러에 의해서 지어졌습니다. 마틴 파울러는 제가 사용하는 스프링의 의존성 주입, 제어의 역전 등의 단어를 정의하였습니다. 이렇게 유명한 엔지니어의 책을 읽게 된다는게 살짝 설렙니다.

그럼 서론은 여기까지하고 바로 책으로 들어가보도록 하겠습니다.

 

일단 책의 첫 번째 챕터는 전체적인 리팩터링이 이런 것이다라고 가르쳐주는 챕터였습니다.

그러다보니 복합적인 내용이 많이 들어가있어 조금 어려운 편이였습니다.

그리고 언어가 Javascript로 되어있기 때문에 이해하는데 두 배로 시간이 걸렸습니다.

내용은 간단한 공연료 계산기 프로그램을 만드는 것이였습니다.

바로 첫 번째 코드를 보여드리겠습니다.

function statement(invoices, plays) {
    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구내역 (고객명 : ${invoices.customer})\n`;
    const format = new Intl.NumberFormat("en-US",{style: "currency", currency: "USD", minimumFractionDigits: 2}).format;

    for (let perf of invoices.performances) {
        const play = plays[perf.playID];
        let thisAmount = 0;

        switch (play.type) {
            case "tragedy" :
                thisAmount = 40000;
                if (perf.audience > 30) {
                    thisAmount += 1000 * (pref.audience - 30);
                }
                break ;
            case "comedy" :
                thisAmount = 30000;
                if (perf.audience > 20) {
                    thisAmount += 10000 + 500 * (pref.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;
}

입력은 따로 JSON파일으로 만들어져 있습니다. 이 함수를 실행시키면 결과는 다음과 같습니다.

청구 내역 (고객명 : BigCo)
	Hamlet: $650.00(55석)
    As You Like It : $580.00(35석)
    Othello : $500.00(40석)
   총액 : $1730.00
   적립 포인트 : 47점

이제 위 코드를 리팩터링하면서 차근차근 살펴보겠습니다.

저희는 크게 구조적인 부분기능적인 부분에서 리팩터링을 할 것입니다. 그러면 여러분은 어느 부분을 먼저 하겠습니까?

바로 "구조"를 정리하고 기능을 추가 및 변경하시기를 추천드립니다. 

일단 기능을 변경하려면 그 기능을 쉽게 찾을 수 있어야하는데 무엇을 건드려야 할지 찾기 어렵다면 버그가 생길 확률이 높아집니다.

그래서 "구조"를 먼저 정리하고 "기능"을 정리하는 방향으로 리팩터링을 해보겠습니다.

"프로그램이 새로운 기능을 추가하기에 불편한 구조라면 먼저 기능 추가가 쉬운 형태로 리팩터링하고 원하는 기능을 추가한다"

 

위의 예시에서 청구 내역을 HTML로 출력하는 기능이 필요합니다. 그럼 HTML태그로 문자열을 감싸면 됩니다.그럼 statement함수를 복사해서 붙이면 될까요? 안됩니다. 왜냐하면 나중에 청구서 로직 변경 할 경우마다 기존함수 및HTML버전 함수들을 하나하나 다 고쳐줘야됩니다. 그리고 무조건 로직은 변경되기 때문에 리팩터링을 할 수 밖에 없습니다.

 

그럼 리팩터링을 단계적으로 실행해보겠습니다. 처음에는 "테스트 코드"를 무조건 구축해둬야합니다.이것을 하나하나 바꿀 때마다 돌려봐야합니다. 비록 시간이 많이 걸리겠지만 전체적으로는 오히려 시간이 덜 낭비될 수 있습니다.이제 진짜 코드로 설명드리겠습니다. 모든 과정을 코드로 담으려니 너무 많은 코드량때문에 일부분만 보여드리겠습니다.

 

함수를 추출하고 싶은 경우 함수 내에서 고정 값을 가지는 변수와 동적인 값을 가지는 변수를 확실하게 알아야합니다.값이 바뀌지 않는 변수는 매개변수로 전달해줍니다. 그리고 동적 값은 고정 값을 가진 변수에 의해 만들어진다는 사실을 기억합니다.그럼 매개변수를 고정 값만 넘겨서 동적 값을 만들면 되겠죠? 그럼 동적 값을 만드는 함수를 만들어줍니다.이런 과정을 책에서는 "임시 변수를 질의 함수로 바꾸기 -> 변수 인라인 하기"로 나타내고 있습니다.

 

위의 방식으로 적립 포인트 구하는 함수(volumeCreditsFor), 포맷 변수제거(usd)를 하였습니다.그러나 반복문을 돌 때마다 값을 누적하는 변수는 어떻게 처리하면 좋을까요? 다음과 같은 순서로 리팩터링합니다.

 

1. 반복문 쪼개기(반복문 분리)

for (let perf of invoices.performances) {
	result += `${play.name}: ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
	totalAmount += amountFor(perf);
}
for (let perf of invoices.performances) {
	volumeCredits += volumeCreditsFor(perf);
}

2. 문장 슬라이드하기(변수 초기화를 누적 코드 앞으로)

for (let perf of invoices.performances) {
	result += `${play.name}: ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
	totalAmount += amountFor(perf);
}
let volumeCredits = 0;
for (let perf of invoices.performances) {
	volumeCredits += volumeCreditsFor(perf);
}

3. 함수 추출하기

4. 변수 인라인하기

 

위의 모든 과정을 거치고 탄생한 코드는 다음과 같습니다.

function statement(invoice, plays) {
    let result = `청구내역 (고객명 : ${invoice.customer})\n`;
    for (let perf of invoice.performances) {
        result += `${palyFor(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 invoice.performances) {
            result += amountFor(perf);
        }
        return result;
    }

    function totalVolumeCredits() {
        let result = 0;
        for (let perf of invoice.performances) {
            result += volumeCreditsFor(perf);
        }
        return result;
    }

    function volumeCreditsFor(perf) {
        let result = 0;
        result += Math.max(perf.audience - 30, 0);
        if ("comedy" == palyFor(perf))
            result += Math.floor(perf.audience / 5);
        return result;
    }

    function amountFor(aPerformance) {

        let result = 0;

        switch (palyFor(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(`알 수 없는 장르 : ${palyFor(aPerformance).type}`);
        }
        return result;
    }

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

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

 

이제는 기능적인 부분에 대해서 고민해보겠습니다. 위와 같이 구조적으로 한 눈에 들어 올 수 있도록 만들었지만 저것들을

복사해서 HTML파일을 만들면 처음이랑 거의 다를 바가 없습니다. 그럼 어떻게 다시 구성하면 될까요 ?

바로 "단계 나누기" 입니다. 즉 필요한 데이터를 처리하고 그 데이터를 어떻게 렌더링 할 것인지 정해주는 것입니다.

그러면 데이터를 가공하는 부분은 한꺼번에 수정이 되기 때문에 효율적인 코드라고 할 수 있습니다.

 

첫 번째로는 렌더하는 함수를 뽑아내겠습니다. 오직 렌더링하는 부분에만 집중하게 말이죠.

function statement(invoice, plays) {
    return renderPlainText(statementData, plays);
}

function renderPlainText(data, plays) {
    let result = `청구내역 (고객명 : ${data.customer})\n`;
    for (let perf of data.performances) {
        result += `${perf.play.name}: ${usd(perf.amount)} (${perf.audience}석)\n`;
    }
    result += `총액 : ${usd(data.totalAmount)}\n`;
    result += `적립 포인트 : ${data.totalVolumeCredits}점\n`;
    return result;
}

function htmlStatement(invoice, plays) {
    return renderHtml(statementData, plays);
}

function renderHtml(data) {
    let result = `<h1>청구 내역 (고객명: ${data.customer})</h1>\n`;
    result += "<table>\n";
    result += "<tr><th>연극</th><th>좌석 수</th><th>금액</th></tr>";
    for (let perf of data.performances) {
        result += ` <tr><td>${perf.play.name}</td><td>(${perf.audience}석)</td>`;
        result += `<td>${usd(perf.amount)}</td></tr>\n`;
    }
    result += "</table>\n";
    result += `<p>총액 : <em>${usd(data.totalAmount)}</em></p>\n`;
    result += `<p>적립 포인트 : <em>${data.totalVolumeCredits}</em>점</p>\n`;
}

두 번째로는 데이터를 생성하는 부분을 따로 파일을 생성해주겠습니다. 그리고 그 곳에 다음과 같은 함수를 만듭니다.

export default function createStatementData(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    statementData.totalAmount = totalAmount(statementData);
    statementData.totalVolumeCredits = totalVolumeCredits(statementData);
    return statementData;

    function enrichPerformance(aPerformance) {
        const result = Object.assign({}, aPerformance);
        result.play = palyFor(result);
        result.amount = amountFor(result);
        result.volumeCredits = volumeCreditsFor(result);
        return result;
    }
    function palyFor(aPerformance) {
        return plays[aPerformance.playID];
    }
    function amountFor(aPerformance) {

        let result = 0;

        switch (aPerformance.play.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(`알 수 없는 장르 : ${aPerformance.play.type}`);
        }
        return result;
    }
    function volumeCreditsFor(aPerformance) {
        let result = 0;
        result += Math.max(aPerformance.audience - 30, 0);
        if ("comedy" == aPerformance.play.type)
            result += Math.floor(aPerformance.audience / 5);
        return result;
    }
    function totalAmount(data) {
        return data.performances.reduce((total,p) => total + p.amount, 0);
    }
    function totalVolumeCredits(data) {
        return data.performances.reduce((total, p) => total + p.volumeCredits, 0);
    }
}

이렇게 기능적인 부분을 나누니 나중에 로직을 변경하더라도 쉽게 변경 할 수 있는 장점이 있습니다.

데이터 생성하는 함수는 전체적으로 데이터 덩어리를 만들어서 렌더링 할 경우 쉽게 사용 할 수 있도록 하는 것이 목표입니다.

 

이렇게 코드를 생성하였는데 장르에 따른 다른 계산 프로그램을 실행시키고 싶은 요구가 들어왔습니다. 이렇게 될 경우 로직으로도

가능하지만 반복적인 코드들이 많아지기 때문에 부모에서 자식으로 상속해주는 방향으로 코드를 변경하겠습니다.

이 때 필요한 기능이 "다형성" 입니다. 다형성의 진짜 힘은 반복적인 코드를 없앨 뿐만 아니라 내가 원하는 기능을 오버라이딩, 오버로딩으로 각 객체에 맞게 입맛대로 구성할 수 있다는 것입니다. 그럼 다형성을 구현해보겠습니다.

 

일단 클래스를 하나 생성하겠습니다. 그리고 추후의 모습까지 간략하게 나타내겠습니다.

class PerformanceCalculator {
    constructor(aPerformance, aPlay) {
        this.performances = aPerformance;
        this.play = aPlay;
    }
    
    get amount() {
    }
    
    get volumeCredits() {
    }
}

class TragedyCalculator extends PerformanceCalculator {
    get amount() {	
    }
}

class ComedyCalculator extends PerformanceCalculator {
    get amount() {
    }
    get volumeCredits() {
    }
}

이렇게 각자의 입맛에 맞게 조절하셔서 해당 객체를 생성하고 싶을 때 생성시키면 되겠습니다. 다음과 같이 말이죠.

    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(`알 수 없는 장르 : ${aPlay.type}`);
        }
    }

처음으로는 위의PerformanceCalculator라는 클래스를 만듭니다.

제가 계산 방식을 다르게 하고싶은 부분은 공연료 계산(amountFor())부분과 적립 포인트 계산(volumeCredits)부분입니다.

이 부분을 클래스 내에서 바꿔주기 위해서 로직을 들고와야합니다. 그리고 나중에 사용 할 경우는 (계산기이름).amount방식으로

사용하도록 해야합니다.

 

이렇게 간략하게 구조적인 리팩터링, 기능적인 리팩터링, 다형성을 이용한 기능 적용 등을 알아봤습니다.

이렇게 리팩터링을 하고나니 변경하고 싶은 부분을 쉽게 변경할 수 있는 큰 장점을 가지게 되었습니다.

다음 장은 진짜 리팩터링이 뭔지 정의에 대해서 알아보는 시간을 가지겠습니다. 마지막 명언으로 포스팅 마무리하겠습니다.

 

"좋은 코드를 가늠하는 확실한 방법은 '얼마나 수정하기 쉬운가'이다."

 

반응형