본문 바로가기
Book/Refactoring 2nd

1. 리팩터링: 첫 번째 예시

by Soono991 2023. 1. 8.

이 포스팅은 리팩터링 2판으로 학습한 내용을 토대로 정리한 포스팅입니다.

 

리팩터링이란 무엇일까?

https://ko.wikipedia.org/wiki/%EB%A6%AC%ED%8C%A9%ED%84%B0%EB%A7%81

 

그렇다면 리팩토링을 왜 해야 할까?

앞서 리팩토링의 설명에서 가독성과 유지보수를 편하게 라는 단어가 나오는데 이 2개의 단어가 왜 중요할까?

우리는 대부분 혼자서 개발을 하지 않고 다른 사람들과 협업을 통해 개발을 하게 됩니다.

 

이때 협업을 보다 편하게 하려면 내가 작성한 코드가 다른 사람들에게 보다 쉽게 읽혀야 하고(가독성)

기능을 추가함에 있어서 기존에 작성해둔 코드를 크게 변경하지 않고 기능을 추가할 수 있어야 한다(유지보수를 편하게)

 

그러면 리팩토링을 함으로써 얻게 되는 이점, 또는 결과는 무엇일까?

 

개인적으로는 비용이라고 생각합니다.

가독성이 높아지고, 유지보수가 편해진다는 것은 시간이 절약된다는 뜻이고, 개발자들에게 시간은 곧 비용이기 때문입니다.

 

그리고 비용을 절약하는 개발자가 곧 능력 있는 개발자이기 때문에 리팩토링을 하지 않아도 개발을 할 수 있지만 능력 있는 개발자가 되기 위해서는 반드시 리팩토링을 잘할 수 있어야 한다고 생각합니다.

 

리팩토링 2판 들어가며,

리팩터링 2판은 처음부터 역사나 여러 원칙 등을 나열하게 되면 지루하고 졸리기 때문에 1장에서 바로 예제로 시작한다고 하는데,

바로 예제를 접해 저자의 의도대로 지루하지 않았고, 또 리팩토링 과정의 전체적인 흐름을 볼 수 있어서 좋았습니다.

 

1장에서는 공연료 청구 프로그램이라는 코드를 리팩토링 하는 과정을 하나씩 설명해 줍니다.

export {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 = 0;

        switch (play.type) {
            case 'tragedy': // 비극
                thisAmount = 40000;
                if (perf.audience > 30) {
                    thisAmount += 10000 * (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);

        // 희극 관객 5명마다 추가 포인트 제공
        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;
}

위 코드는 일단 잘 동작하는 코드인데, 혹시 나는 리팩토링에 있어서 아래와 같은 생각을 하지는 않았나 생각해 봅니다.

프로그램이 잘 작동하는 상황에서 그저 코드가 '지저분하다'는 이유로 불평하는 것은 프로그램의 구조를 너무 미적인 기준으로만 판단하는 건 아닐까? 컴파일러는 코드가 깔끔하든 지저분하든 개의치 않으니 말이다.
(p.26)

 

충분히 공감 가는 지적입니다. 특정한 이유가 아닌 단순히 지저분해 보인다는 느낌에서 코드를 수정하려고 하는 것은 오히려 더 설계를 나쁘게 만들 수도 있으며 나중에 버그가 생길 가능성도 높아질 수 있습니다.

 

기능 추가

 

첫 번째로 청구 내역을 HTML로 출력하는 기능을 추가한다고 가정해 봅니다.

이 경우 statement() 함수에서 어떤 부분이 변경되어야 하는지 생각해 보는데, 청구 내역을 출력하는 부분을 각각 조건문으로 감싸야하며, 청구 내역을 조건문으로 분기하기 위한 변수도 전달되어야 할 것입니다.

아니면 청구 내역을 HTML로 출력하는 기능을 별도의 htmlStatement() 함수로 분리할 수도 있는데, 이 경우 청구 내역을 출력하는 기능 외에 다른 코드들이 중복되는 사태가 발생합니다.

 

두 번째로 배우들은 사극, 전원극, 전원 희극, 역사 전원극, 역사 비극, 희비 역사 전원극, 장면 변화가 없는 고전극, 길이와 시간과 장소에 제약 없는 자유극등 더 많은 장르를 연기하고 싶어 한다고 가정해 봅니다.

그렇게 되면 연극 장르, 공연료 정책이 달라질 때마다 statement() 함수를 수정해야 하기 때문에,

statement() 함수 자체가 굉장히 복잡해지게 됩니다.

 

따라서 마틴 파울러 님은 기능 추가 및 리팩토링에 있어 아래와 같이 조언합니다.

프로그램이 새로운 기능을 추가하기에 편한 구조가 아니라면, 먼저 기능을 추가하기 쉬운 형태로 리팩터링 하고 나서 원하는 기능을 추가합니다.
(p.27)

그래서 먼저 statement() 함수를 기능을 추가하기 쉬운 형태로 수정해 보겠습니다.

연극 장르를 분기 처리 하는 switch문을 수정해 보겠습니다.

switch (play.type) {
    case 'tragedy': // 비극
        thisAmount = 40000;
        if (perf.audience > 30) {
            thisAmount += 10000 * (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}`);
}

switch문을 살펴보면 한 번의 공연에 대한 요금을 계산하고 있습니다.

워드 커닝햄이 말하길, 이런 식으로 파악한 정보는 휘발성이 높기로 악명 높은 저장 장치인 내 머릿속에 기록되므로, 잊지 않으려면 재빨리 코드에 반영해야 한다.
그러면 다음번에 코드를 볼 때, 다시 분석하지 않아도 코드 스스로가 자신이 하는 일이 무엇인지 이야기해줄 것이다.
(p.30)

워드 커닝햄의 조언에 따라 연극 장르에 따라 요금을 계산하는 switch문을 별도의 함수로 분리해 보겠습니다.

이러한 리팩토링 절차를 이 책에서는 함수 추출하기라고 부르고 있습니다.

function amountFor(aPerformance, play) {
    let result = 0;

    switch (play.type) {
        case 'tragedy': // 비극
            result = 40000;
            if (aPerformance.audience > 30) {
                result += 10000 * (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(`알 수 없는 장르: ${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);

        // 희극 관객 5명마다 추가 포인트 제공
        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;
}

이렇게 한 단계 수정을 거쳤다면 반드시 테스트를 해보라고 권장합니다.

아무리 간단한 수정이라도 리팩터링 후에는 항상 테스트하는 습관을 들이는 것이 바람직하다.
사람은 실수하기 마련이다. 적어도 내가 겪은 바로는 그렇다. 한 가지를 수정할 때마다 테스트하면, 오류가 생기더라도 변경 폭이 작기 때문에 살펴볼 범위도 좁아서 문제를 찾고 해결하기가 훨씬 쉽다.
...
한 번에 너무 많이 수정하려다 실수를 저지르면 디버깅하기 어려워서 결과적으로 작업 시간이 늘어난다.
조금씩 수정하여 피드백 주기를 짧게 가져가는 습관이 이러한 재앙을 피하는 길이다.

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

컴퓨터가 이해하는 코드는 바보도 작성할 수 있다. 사람이 이해하도록 작성하는 프로그래머가 진정한 실력자다.
(p.35)

다시 코드를 살펴보면 play는 개별 공연에서 얻기 때문에 매개변수로 전달할 필요가 없습니다. 따라서 매개변수를 제거하고 별도의 질의 함수를 작성합니다.

이 방법을 임시 변수를 질의 함수로 바꾸기라고 부릅니다.

function playFor(aPerformance) {
    return plays[aPerformance.playID];
}
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 = playFor(perf);                 // 질의 함수로 변경
        let thisAmount = amountFor(perf, play);
        // 포인트 적립
        volumeCredits += Math.max(perf.audience - 30, 0);

        // 희극 관객 5명마다 추가 포인트 제공
        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;
}

리팩토링을 진행했으니 다시 테스트를 통해 기능이 정상적으로 동작하는지 확인합니다.

그리고 코드를 다시 보면 질의 함수를 통해 얻은 play라는 변수가 사실 amountFor() 함수의 매개변수와 청구 내역 출력에서만 사용됩니다..

이 경우 playFor()를 통해 얻은 결과를 굳이 변수로 저장하지 않고 바로 사용할 수 있습니다.

이 과정을 변수 인라인하기라고 부릅니다.

function statement(invoice, plays) {
    ...

    for (let perf of invoice.performances) {
        let thisAmount = amountFor(perf); // 매개변수 제거, amountFor() 에서 playFor() 호출
        // 포인트 적립
        volumeCredits += Math.max(perf.audience - 30, 0);

        // 희극 관객 5명마다 추가 포인트 제공
        if ('comedy' === play.type) {
            volumeCredits += Math.floor(perf.audience / 5);
        }

        // 청구 내역 출력
        // 변수 인라인하기
        result += ` ${playFor(perf).name} : ${format(thisAmount / 100)} (${perf.audience} 석)\n`;
        totalAmount += thisAmount;
    }

    result += `총액: ${format(totalAmount / 100)}\n`;
    result += `적립 포인트: ${volumeCredits}점\n`;
    return result;
}
function amountFor(aPerformance) {
    let result = 0;

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

    return result;
}

그리고 thisAmount 도 변수 인라인하기로 제거해 보겠습니다.

function statement(invoice, plays) {
    ...

    for (let perf of invoice.performances) {
        ...

        totalAmount += amountFor(perf); // 변수 인라인하기
    }

    ...
}

다음은 포인트를 적립하는 코드도 함수 추출하기를 통해 리팩토링 할 수 있을 것 같습니다.

...
// 포인트 적립
volumeCredits += Math.max(perf.audience - 30, 0);

// 희극 관객 5명마다 추가 포인트 제공
if ('comedy' === playFor(perf).type) {
    volumeCredits += Math.floor(perf.audience / 5);
}

...
function statement(invoice, plays) {
    ...
    for (let perf of invoice.performances) {
        // 포인트 적립
        volumeCredits += volumeCreditsFor(perf); // 함수 추출
        // 청구 내역 출력
        result += ` ${playFor(perf).name} : ${format(amountFor(perf) / 100)} (${perf.audience} 석)\n`;
        totalAmount += amountFor(perf);
    }
    ...
}
function volumeCreditsFor(aPerformance) {
    let result = 0;
    result += Math.max(aPerformance.audience - 30, 0);

    // 희극 관객 5명마다 추가 포인트 제공
    if ('comedy' === playFor(aPerformance).type) {
        result += Math.floor(aPerformance.audience / 5);
    }
    return result;

}

format 변수도 함수 추출하기 & 변수 인라인하기를 통해서 리팩토링 해보겠습니다.

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

    result += `총액: ${usd(totalAmount)}\n`;

    ...
}

지금까지 리팩토링 과정을 보면 비슷한 흐름들을 통해 리팩토링이 진행되는 것을 느낄 수 있습니다.

제 개인적인 생각으로는 이렇게 비슷한 패턴들을 계속 반복하면서 숙달되게 하려고 일부러 이렇게 예제를 작성하지는 않았을까? 하는 생각이 들었습니다.

 

이후에도 계속해서 비슷한 패턴으로 리팩토링을 진행합니다.

  • 변수 인라인하기
  • 함수 추출하기
  • 문장 슬라이드하기
  • 반복문 쪼개기
  • 반복문을 파이프라인으로 바꾸기
  • 매개변수 대신 객체 전달하기
  • 조건부 로직을 다형성으로 바꾸기
  • 함수 인라인하기
  • 생성자를 팩토리 함수로 바꾸기

1장에서 사용한 리팩토링 종류들입니다. 그리고 뒤에서는 각각의 리팩토링 방법들을 별도의 예제로 다시 학습하며 각 리팩토링 방법의 사용 방법과 의미를 학습합니다.

 

리팩토링 책을 통해 직접 리팩토링 종류들과 적용 방법에 대해 알고 나니 코드를 리팩토링 할 때 보다 저자가 우려했던 미적 감각에 의해서가 아니라 정확한 목적의미를 가지고 접근할 수 있게 되어서 좋았습니다.

 

마지막으로 저자이신 마틴 파울러 님의 리팩토링에 대한 생각으로 마무리하겠습니다.

좋은 코드를 가늠하는 확실한 방법은 `얼마나 수정하기 쉬운가`이다.
(p.76)

이 책은 코드를 개선하는 방법을 다룬다. 그런데 프로그래머 사이에서 어떤 코드가 좋은 코드인지에 대한 의견은 분분하다. 내가 선호하는 `적절한 이름의 작은 함수들`로 만드는 방식에 반대하는 사람도 분명 있을 것이다.
미적인 관점으로 접근하면 좋고 나쁨이 명확하지 않아서 개인 취향 말고는 어떠한 지침도 세울 수 없게 된다.
하지만 나는 취향을 넘어서는 관점이 분명 존재하며, 코드를 `수정하기 쉬운 정도`야말로 좋은 코드를 가늠하는 확실한 방법이라고 믿는다.
코드는 명확해야 한다.
코드를 수정해야 할 상황이 되면 고쳐야 할 곳을 쉽게 찾을 수 있고 오류 없이 빠르게 수정할 수 있어야 한다.
건강한 코드베이스는 생산성을 극대화하고, 고객에게 필요한 기능을 더 빠르고 저렴한 비용으로 제공하도록 해준다.
(p.76)

이번 예시를 통해 배울 수 있는 가장 중요한 것은 바로 리팩터링 하는 리듬이다.
...
리팩터링을 효과적으로 하는 핵심은, 단계를 잘게 나눠야 더 빠르게 처리할 수 있고, 코드는 절대 깨지지 않으며, 이러한 작은 단계들이 모여서 상당히 큰 변화를 이룰 수 있다는 사실을 깨닫는 것이다. 이 점을 명심하고 그대로 따라주기 바란다.
(p.77)

'Book > Refactoring 2nd' 카테고리의 다른 글

6~12. 리팩터링  (0) 2023.01.08
4. 테스트 구축하기  (0) 2023.01.08
3. 코드에서 나는 악취  (0) 2023.01.08
2. 리팩터링 원칙  (0) 2023.01.08

댓글