[Refactoring] 리팩터링 2판 Chapter.01 (단계 쪼개기)

Date:     Updated:

카테고리:

태그:

지난 포스팅에서 리펙토링 한 코드 입니다.

function statement(invoice, plays) {
    let result = '청구 내역 (고객명: ${invoice.customer})\n'

    for (let perf of invoice.performances) {
        //청구 내역을 출력함
        result += '${playFor(perf).name}: ${usd(amountFor(perf)/100)} (${perf.audience}석)\n';
    }//for문 종료

    let totalAmount = totalAmount();//->반복문 쪼개기

    result += '총액: ${usd(totalAmount/100)}\n';
    result += '적립 포인트 : ${totalVolumeCredits(perf)}점\n';//-> 변수 인라인 하기
    return result;

    function volumeCreditsFor(aPerformance) {
        let volumeCredits = 0;

        volumeCredits += Math.max(aPerformance.audience - 30, 0);
        if ("comedy" == playFor(aPerformance).type) // -> 변수 인라인 하기{
            volumeCredits += Math.floor(aPerformance.audience / 5);

        return volumeCredits;
    }

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

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

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

    function amountFor(aPerformance) {
        let result = 0;

        switch (playFor(aPerformance).type) {
            case "tragedy": //비극
                thisAmount = 4000;
                if (aPerformance.audience > 30) {
                    thisAmount += 1000 * (aPerformance.audience - 30);
                }
                break;
            case "comedy":
                thisAmount = 3000;
                if (aPerformance.audience > 20) {
                    thisAmount += 10000 + 500 * (aPerformance.audiece - 20);
                }
                thisAmount += 300 * aPerformance.audience;
                break;

            default:
                throw new Error('알 수 없는 장르: ${playFor(aPerformance).type}');
        }

        return result;//결과값 반환
    }

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

위 코드를 살펴보면, 리팩터링을 통해 코드의 구조가 한결 나아진 것을 볼 수 있습니다. 원래는 statement()라는 함수 하나였지만, 지금은 함수가 늘어나긴 했지만, statement()코드는 총 8줄로 구성되어 있습니다. 결론적으로, 각 계산 과정은 물론, 전체 흐름을 이해하기가 훨씬 쉬워졌습니다.

지금까지의 리팩터링은 코드의 논리적인 요소를 파악하기 쉽도록 하는것이 목적이었습니다. 이번 포스팅에서는 코드의 수행 단계에 따라서 리팩터링을 해봅니다. 저자는 이 과정을 단계 쪼개기라고 표현 합니다. (현재 statement() 는 HTML 에 표현할 문자열을 생성하는 역할을 함. 이제부터 statement() 이 문자열을 HTML 에 표현하기 위한 작업을 함.)

우선, 함수 추출하기를 사용합니다. statement()는 현재 텍스트를 출력하는 역할을 하고 있습니다. 이것을 renderPlainText()라는 함수로 추출 합니다. 이 과정에서 중첩함수와 statement()는 분리하도록 합니다.

//중첩함수 분리 (renderPlainText 에서 계산한 결과값만 반환 받을 것임)
function statement(invoice, plays) {
    return renderPlainText(invoice, plays);
}


function renderPlainText(invoice, plays) {
    let result = '청구 내역 (고객명: ${invoice.customer})\n'

    for (let perf of invoice.performances) {
        //청구 내역을 출력함
        result += '${playFor(perf).name}: ${usd(amountFor(perf)/100)} (${perf.audience}석)\n';
    }//for문 종료

    let totalAmount = totalAmount();//->반복문 쪼개기

    result += '총액: ${usd(totalAmount/100)}\n';
    result += '적립 포인트 : ${totalVolumeCredits(perf)}점\n';//-> 변수 인라인 하기
    return result;


    function volumeCreditsFor(aPerformance) {
        let volumeCredits = 0;

        volumeCredits += Math.max(aPerformance.audience - 30, 0);
        if ("comedy" == playFor(aPerformance).type) // -> 변수 인라인 하기{
            volumeCredits += Math.floor(aPerformance.audience / 5);

        return volumeCredits;
    }

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

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

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

    function amountFor(aPerformance) {
        let result = 0;

        switch (playFor(aPerformance).type) {
            case "tragedy": //비극
                thisAmount = 4000;
                if (aPerformance.audience > 30) {
                    thisAmount += 1000 * (aPerformance.audience - 30);
                }
                break;
            case "comedy":
                thisAmount = 3000;
                if (aPerformance.audience > 20) {
                    thisAmount += 10000 + 500 * (aPerformance.audiece - 20);
                }
                thisAmount += 300 * aPerformance.audience;
                break;

            default:
                throw new Error('알 수 없는 장르: ${playFor(aPerformance).type}');
        }

        return result;//결과값 반환
    }

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

그리고, 두 단계 사이의 중간 데이터 구조 역할을 할 객체를 만들어서 renderPlainText() 에 인수로 전달 합니다. 이 과정에서 renderPlainText()함수에 필요한 값들을 statement()에서 계산하여 전달하는 과정을 수행 합니다.

그러기 위해서 renderPlainText()안에 있는 중첩함수들을 다시 statement()의 중첩함수로 추출 합니다. 이 과정에서 enrichPerformance()라는 함수를 생성하여 중간 데이터를 전달하게 만듭니다. 여기서 저자는 다음과 같이 말합니다.

데이터를 전달 할 때, 가변 데이터는 금방 상하기 때문에 최대한 불변 처럼 취급한다.

그래서 enrichPerformance()함수는 얕은 복사를 사용하여 구성합니다.

해당 과정을 거친 코드는 아래와 같습니다.

//중첩함수 분리 (renderPlainText 에서 계산한 결과값만 반환 받을 것임)
function statement(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    statementData.totalAmount = totalAmount(statementData);//totalAmount 함수를 statement의 중첩함수로 만들어서 중간 데이터 생성
    statementData.totalVolumeCredits = totalVolumeCredits(statementData);//totalVolumeCredits 함수를 statement의 중첩함수로 만들어서 중간 데이터 생성
    return renderPlainText(statementData, plays);

    function enrichPerformance(aPerformance) {
        const result = Object.assign({}, aPerformance);//얕은 복사 수행
        result.play = playFor(result);//playFor 함수를 statement의 중첩함수로 만들어서 중간 데이터 생성
        result.amount = amountFor(result);//amountFor 함수를 statement의 중첩함수로 만들어서 중간 데이터 생성
        result.volumeCredits = volumeCreditsFor(result);//volumeCreditsFor 함수를 statement의 중첩함수로 만들어서 중간 데이터 생성
        return result;
    }

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

    function amountFor(aPerformance) {
        let result = 0;

        switch (aPerformance.play.type) {
            case "tragedy": //비극
                thisAmount = 4000;
                if (aPerformance.audience > 30) {
                    thisAmount += 1000 * (aPerformance.audience - 30);
                }
                break;
            case "comedy":
                thisAmount = 3000;
                if (aPerformance.audience > 20) {
                    thisAmount += 10000 + 500 * (aPerformance.audiece - 20);
                }
                thisAmount += 300 * aPerformance.audience;
                break;

            default:
                throw new Error('알 수 없는 장르: ${playFor(aPerformance).type}');
        }

        return result;//결과값 반환
    }

    function volumeCreditsFor(aPerformance) {
        let volumeCredits = 0;

        volumeCredits += Math.max(aPerformance.audience - 30, 0);
        if ("comedy" == aPerformance.play.type) // -> 변수 인라인 하기{
            volumeCredits += Math.floor(aPerformance.audience / 5);

        return volumeCredits;
    }

    function totalAmount(data) {
        // let result = 0;
        // for (let perf of data.performances) {
        //     result += perf.amount;
        // }
        // return result;
        return data.performances.reduce((total, p) => total + p.amount, 0);
    }

    function totalVolumeCredits(data) {
        // let result = 0;
        // for (let perf of data.performances) {
        //     result += perf.volumeCredits;
        // }
        // return result;

        return data.performances.reduce((total, p) => total + p.volumeCredits, 0);
    }
}

function renderPlainText(data, plays) {
    let result = '청구 내역 (고객명: ${data.customer})\n'

    for (let perf of invoice.performances) {
        //청구 내역을 출력함
        result += '${perf.play.name}: ${usd(amountFor(perf.amount)/100)} (${perf.audience}석)\n';//playFor 함수 대체
    }//for문 종료

    let totalAmount = totalAmount();//->반복문 쪼개기

    result += '총액: ${usd(data.totalAmount/100)}\n';
    result += '적립 포인트 : ${data.totalVolumeCredits}점\n';//-> 변수 인라인 하기
    return result;

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

다음으로는, 반복문을 파이프라인으로 바꾸기 를 적용합니다.

   function totalAmount(data) {
        // let result = 0;
        // for (let perf of data.performances) {
        //     result += perf.amount;
        // }
        // return result;
        return data.performances.reduce((total, p) => total + p.amount, 0);
    }

    function totalVolumeCredits(data) {
        // let result = 0;
        // for (let perf of data.performances) {
        //     result += perf.volumeCredits;
        // }
        // return result;

        return data.performances.reduce((total, p) => total + p.volumeCredits, 0);
    }

C#의 Linq 와 같은 역할을 하는 식으로 나타내어 코드 라인수를 줄이고, 가독성을 향상시키는것 같습니다.

이제, 이렇게 하고 나니, statement() 함수가 꽤나 분량이 많아졌습니다. 중첩함수를 분리해서, 값을 계산하는 부분과 분리해 줍니다.

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

function createStatementData(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    statementData.totalAmount = totalAmount(statementData);//totalAmount 함수를 statement의 중첩함수로 만들어서 중간 데이터 생성
    statementData.totalVolumeCredits = totalVolumeCredits(statementData);//totalVolumeCredits 함수를 statement의 중첩함수로 만들어서 중간 데이터 생성

//...여러 중첩 함수들....
}

이제, 별도의 파일로 저장하도록 합니다. createStatementData.js를 만들어줍니다. 그리고 분리된 statement.js 파일에 html 을 렌더링 할 수 있는 코드도 작성 합니다. (이 과정에서 usd 함수는 html을 렌더링 할 때도 사용할 수 있도록 최상위로 옮김)

🗅 statement.js

import createStatementData from './createStatementData.js'

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

function renderPlainText(data, plays) {
    let result = '청구 내역 (고객명: ${data.customer})\n'

    for (let perf of invoice.performances) {
        //청구 내역을 출력함
        result += '${perf.play.name}: ${usd(amountFor(perf.amount)/100)} (${perf.audience}석)\n';//playFor 함수 대체
    }//for문 종료

    let totalAmount = totalAmount();//->반복문 쪼개기

    result += '총액: ${usd(data.totalAmount/100)}\n';
    result += '적립 포인트 : ${data.totalVolumeCredits}점\n';//-> 변수 인라인 하기
    return result;
}

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

function renderHtml(data) {
    let result = `<h1>Statement for ${data.customer}</h1>\n`;
    result += "<table>\n";
    result += "<tr><th>play</th><th>seats</th><th>cost</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>Amount owed is <em>${usd(data.totalAmount)}</em></p>\n`;
    result += `<p>You earned <em>${data.totalVolumeCredits}</em> credits</p>\n`;
    return result;
}

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

🗅 createStatementData.js

function createStatementData(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    statementData.totalAmount = totalAmount(statementData);//totalAmount 함수를 statement의 중첩함수로 만들어서 중간 데이터 생성
    statementData.totalVolumeCredits = totalVolumeCredits(statementData);//totalVolumeCredits 함수를 statement의 중첩함수로 만들어서 중간 데이터 생성


    function enrichPerformance(aPerformance) {
        const result = Object.assign({}, aPerformance);//얕은 복사 수행
        result.play = playFor(result);//playFor 함수를 statement의 중첩함수로 만들어서 중간 데이터 생성
        result.amount = amountFor(result);//amountFor 함수를 statement의 중첩함수로 만들어서 중간 데이터 생성
        result.volumeCredits = volumeCreditsFor(result);//volumeCreditsFor 함수를 statement의 중첩함수로 만들어서 중간 데이터 생성
        return result;
    }

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

    function amountFor(aPerformance) {
        let result = 0;

        switch (aPerformance.play.type) {
            case "tragedy": //비극
                thisAmount = 4000;
                if (aPerformance.audience > 30) {
                    thisAmount += 1000 * (aPerformance.audience - 30);
                }
                break;
            case "comedy":
                thisAmount = 3000;
                if (aPerformance.audience > 20) {
                    thisAmount += 10000 + 500 * (aPerformance.audiece - 20);
                }
                thisAmount += 300 * aPerformance.audience;
                break;

            default:
                throw new Error('알 수 없는 장르: ${playFor(aPerformance).type}');
        }

        return result;//결과값 반환
    }

    function volumeCreditsFor(aPerformance) {
        let volumeCredits = 0;

        volumeCredits += Math.max(aPerformance.audience - 30, 0);
        if ("comedy" == aPerformance.play.type) // -> 변수 인라인 하기{
            volumeCredits += Math.floor(aPerformance.audience / 5);

        return volumeCredits;
    }

    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);
    }
}

이렇게 나누고 보니, 코드량이 처음보다 부쩍 는것을 볼 수 있습니다. 코드량이 단순히 늘어나기만 했다면 이것은 잘못된 것이지만, 코드량이 증가함으로써 얻은 이점이 많습니다. 코드의 논리적인 구조, 동작 단계를 명확히 구분할 수 있게 되었습니다. 이렇게 모듈화가 되면 각 부분이 하는 일과 그 부분들이 맞물려 돌아가는 과정을 파악하기 쉬워집니다.

이렇게 깔끔하게 코드를 유지하라고 저자는 당부합니다.

캠핑자들에게는 “도착했을 때보다 깔끔하게 정돈하고 떠난다” 는 규칙이 있다. 프로그래밍도 마찬가지다. 항시 코드베이스를 작업 시작 전보다 건강하게 만들어놓고 떠나야 한다.

어떻게보면 아주 당연한 말이지만, 저자가 이토록 강조하는 것은 당연한것을 지키지 않는 사람이 더 많기 때문이라고 생각합니다. 우선, 동작만 되면 다음에… 다음에… 하다가 결국 코드는 병들어 버리기 마련입니다. 물론 주어진 시간내에 프로젝트를 완료하기 위해서는 적당한 타협도 필요하겠지만, 기본은 코드를 깔끔하게 유지하기 가 되어야 한다고 생각합니다.

Refactoring 카테고리 내 다른 글 보러가기

댓글 남기기