리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

함수 안에서 변수를 사용하는 것보다 원래 표현식과 다를바 없는 경우 인라인한다.

예시

function isOver(anOrder){
    let basePrice = anOrder.basePrice;
    return (basePrice > 1000);
}
function isOver(anOrder){
    return (anOrder.basePrice > 1000);
}
 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

표현식이 너무 복잡해서 이해하기 어려울 지역 변수를 활용하면 표현식을 쪼개 관리하기 쉽게 만들 있다.

 

이렇게 추가된 변수는 디버깅에도 도움된다. 디버거 breakpoint 활용할 있기 때문이다.

 

추출된 변수에 이름을 붙일 변수의 문맥을 고려해야한다. 함수 내에서만 의미있다면, 변수로 추출한다. 함수 외부까지 의미가 있다면, 함수로 추출해야 한다.

예시

//기본형
function price(order){
    return order.quantity * order.itemPrice -
        Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
        Math.min(order.quantity * order.itemPrice * 0.1, 100);
}
function price(order){
    const basePrice = order.quantity * order.itemPrice;
    //가격 = 기본 가격 = 수량 할인 + 배송비
    return basePrice -
        Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
        Math.min(order.quantity * order.itemPrice * 0.1, 100);
}
function price(order){
    const basePrice = order.quantity * order.itemPrice;
    //똑같은 표현식 부분 치환
    return basePrice -
        Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
        Math.min(basePrice * 0.1, 100);
}
function price(order){
    const basePrice = order.quantity * order.itemPrice;
    const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
    const shipping = Math.min(basePrice * 0.1, 100);
    return basePrice - quantityDiscount + shipping;
}

사용한 자동 리팩터링

예시: 클래스 안에서

추출하는 대상이 price()메서드 범위를 넘어 Order 클래스 전체에 영향을 미친다. 이럴 변수가 아닌 메서드로 추출한다.

//기본형
class Order {
    constructor(aRecord){
        this._data = aRecord;
    }
    get quantity() {return this._data.quantity;}
    get itemPrice() {return this._data.itemPrice;}
    get price(){
        return this.quantity * this.itemPrice -
        Math.max(0, this.quantity - 500) * this.itemPrice * 0.05 +
        Math.min(this.quantity * this.itemPrice * 0.1, 100);
    }
}
//리팩터링
class Order {
    constructor(aRecord){
        this._data = aRecord;
    }
    get quantity() {return this._data.quantity;}
    get itemPrice() {return this._data.itemPrice;}
    get price(){
        return this.basePrice - this.quantityDiscount + this.shipping;
    }
    get shipping() {
        return Math.min(this.basePrice * 0.1, 100);
    }
    get quantityDiscount() {
        return Math.max(0, this.quantity - 500) * this.itemPrice * 0.05;
    }
    get basePrice() {
        return this.quantity * this.itemPrice;
    }
}

이렇게 메서드로 추출해두면 객체를 데이터를 외부에서 쉽게 활용할 있다.

 

사용한 자동 리팩터링

주의 것은 메서드를 추출해주지 게터 메서드로 만들어 주지 않아 주의해야 한다.

 

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

리팩터링에서 기조는 짧막한 함수를 명료한 이름을 붙여 사용하는 것이다. 때로는 그보다 함수 본문이 이름만큼 명확한 경우도 있다. 이런 경우 간접 호출은 거슬리므로 제거한다.

 

이유로 함수 추출하기로 추출한 경우도 (잘못 추출한 것이므로) 다시 함수 인라인하기로 합칠 수도 있다.

 

간접 호출을 너무 과하게 쓰는 코드도 인라인 대상이다.

 

인라인하기 너무 복잡한 함수는 인라인하기 적용 대상이 아니다.

예시

가장 간단한 예시

function rating(aDriver){
    return moreThanFiveLateDeliveries(aDriver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(aDriver){
    return aDriver.numberOfLateDeliveres > 5;
}
function rating(aDriver){
    return (aDriver.numberOfLateDeliveres > 5) ? 2 : 1;
}

 

매개변수가 조금 다른 경우

//기본형
function reportLines(aCustomer){
    const lines = [];
    gatherCustomerData(lines, aCustomer);
    return lines;
}
function gatherCustomerData(out, aCustomer){
    out.push(["name",aCustomer.name]);
    out.push(["location",aCustomer.location]);
}
//매개변수가 다른 경우 리팩터링
function reportLines(aCustomer){
    const lines = [];
    out.push(["name",aCustomer.name]);
    out.push(["location",aCustomer.location]);
    return lines;
}

 

함수 인라인하기가 너무 복잡하다면 반드시 단계를 나누어 처리하자. 그래야 과정 중간에 테스트 실패 지점에서 버그 구간을 좁게 특정할 있다.

 

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

목적과 구현을 분리하는 방식을 기준으로 삼는다.

코드를 보고 무슨 일을 하는지 파악하는데 함참 걸린다면 부분을 함수로 추출한 '무슨 ' 대한 명확한 이름을 붙인다.

 

장황한 주석이 붙은 코드 뭉치는 함수 추출 대상이 가능성이 높다.

 

일반적으로 함수가 6 이하가 되도록 잘게 유지한다. 이로 인한 성능 저하는 거의 없다.

함수가 짧으면 오히려 컴파일러가 최적화할 기회를 얻는다.

최적화에 대한 고민이 생길 최적화 지침을 상기하자

번째, 하지 마라

번째, 전문가일 경우라도 아직 하지 마라

예시 : 유효범위를 벗어나는 변수가 없을

 

function printOwing(invoice){
    let outstanding = 0;
    console.log("******************");
    console.log("**** 고객 채무 ****")
    console.log("******************");
    //미해결 채무
    for(const o of invoice.orders){
        outstanding += o.amount;
    }
    //마감일
    const today = Clock.today;
    invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()+30);
    console.log(`고객명 : ${invoice.customer}`);
    console.log(`채무액 : ${outstanding}`);
    console.log(`마감일 : ${invoice.dueDate.toLocaleDateString()}`);
}
//유효범위를 벗어나는 변수가 없을 때
function printOwing(invoice){
    let outstanding = 0;
    printBanner();
    //미해결 채무
    for(const o of invoice.orders){
        outstanding += o.amount;
    }
    //마감일
    const today = Clock.today;
    invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()+30);
    printDetails();
    function printDetails() {
        console.log(`고객명 : ${invoice.customer}`);
        console.log(`채무액 : ${outstanding}`);
        console.log(`마감일 : ${invoice.dueDate.toLocaleDateString()}`);
    }
    function printBanner() {
        console.log("******************");
        console.log("**** 고객 채무 ****");
        console.log("******************");
    }
}

리팩터링 란에 존재

 

 

 

예시: 지역 변수를 사용할

지역변수를 사용하지만 다른 값을 다시 대입하지 않을

//지역 변수를 사용할 때
function printOwing(invoice){
    let outstanding = 0;
    printBanner();
    //미해결 채무
    for(const o of invoice.orders){
        outstanding += o.amount;
    }
    //마감일
    const today = Clock.today;
    invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()+30);
    //세부 사항을 출력한다
    console.log(`고객명 : ${invoice.customer}`);
    console.log(`채무액 : ${outstanding}`);
    console.log(`마감일 : ${invoice.dueDate.toLocaleDateString()}`);
    function printBanner() {
        console.log("******************");
        console.log("**** 고객 채무 ****");
        console.log("******************");
    }
}
 //중첩 함수가 지원되지 않는 언어는 이렇게 넣어줘야 할 것
    printDetails(invoice, outstanding);
    function printDetails(invoice, outstanding) {
        console.log(`고객명 : ${invoice.customer}`);
        console.log(`채무액 : ${outstanding}`);
        console.log(`마감일 : ${invoice.dueDate.toLocaleDateString()}`);
    }
recordDueDate(invoice);
    //중첩 함수가 지원되지 않는 언어는 이렇게 넣어줘야 할 것
    printDetails(invoice, outstanding);
    function recordDueDate(invoice) {
        const today = Clock.today;
        invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
    }

 

 

예시: 지역 변수의 값을 변경할

이럴 경우 매개변수가 들어가는 지역 변수에 대한 임시 변수를 새로 하나 만들어 변수를 사용한다.

    let outstanding = 0;
    printBanner();
    //미해결 채무
    for(const o of invoice.orders){
        outstanding += o.amount;
    }
    printBanner();
    //문장 슬라이드로 사용되는 곳 근처로 옮김
    let outstanding = 0;
    for(const o of invoice.orders){
        outstanding += o.amount;
    }
    printBanner();
    let outstanding = 0;
    for(const o of invoice.orders){
        outstanding += o.amount;
    }
    //추출할 부분을 새로운 함수로 복사
    function calculateOutstanding(invoice){
        let outstanding = 0;
        for(const o of invoice.orders){
            outstanding += o.amount;
        }
        return outstanding;
    }
    printBanner();
    //추출한 함수가 반환한 값을 원래 변수에 저장한다.
    let outstanding = calculateOutstanding(invoice);
    function calculateOutstanding(invoice){
        let outstanding = 0;
        for(const o of invoice.orders){
            outstanding += o.amount;
        }
        return outstanding;
    }
    //마지막으로 반환 값의 이름을 코딩 스타일에 맞게 변경
    const outstanding = calculateOutstanding(invoice);
    function calculateOutstanding(invoice){
        let result = 0;
        for(const o of invoice.orders){
            result += o.amount;
        }
        return result;
    }

 

 

function printOwing(invoice){
    printBanner();
    const outstanding = calculateOutstanding(invoice);
   
    recordDueDate(invoice);
    printDetails(invoice, outstanding);
    function calculateOutstanding(invoice){
        let result = 0;
        for(const o of invoice.orders){
            result += o.amount;
        }
        return result;
    }
    function recordDueDate(invoice) {
        const today = Clock.today;
        invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
    }
    function printDetails(invoice, outstanding) {
        console.log(`고객명 : ${invoice.customer}`);
        console.log(`채무액 : ${outstanding}`);
        console.log(`마감일 : ${invoice.dueDate.toLocaleDateString()}`);
    }
    function printBanner() {
        console.log("******************");
        console.log("**** 고객 채무 ****");
        console.log("******************");
    }
}

 

 

 

값을 반환할 변수가 여러 개라면, 추출할 코드를 재구성하는 식으로 처리한다.

하나의 함수가 하나의 값을 반환하는 방식으로 여러 함수로 재구성한다.

 

 

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

 최대한 부가 설치없이 브라우저에서 확인하려 했으나, 부득이하게 개발환경을 구축(4.3 부분)

코드 링크, 변화과정은 history로 commit 확인

 

GitHub - rkwhr0010/refactoring: 리팩터링책 내용

리팩터링책 내용. Contribute to rkwhr0010/refactoring development by creating an account on GitHub.

github.com

 


리팩터링을 제대로 하려면 불파기하게 저지르는 실수를 잡아주는 견고한 테스트 스위트가 뒷받침돼야 한다.

 

테스트 작성은 개발 효율을 높여준다. , 그냥 개발하는 것보다 테스트 코드 작성 시간을 따로 할애하고 개발하는 것이 생산성이 높다는 말이다.

 

4.1 자가 테스트 코드의 가치

개발자는 실제 코드 작성 시간의 비중은 크지 않다. 외에 분석, 설계나 특히 디버깅에 시간을 쓴다. 버그 자체도 발견만 하면 수정은 금방이다. 진짜 시간이 많이드는 것은 버그를 찾는 일이다.

 

코드 변경 마다 자가 테스트 코드를 돌린다면, 버그를 찾기 쉬워진다. 직전까지 테스트가 성공했으면 이후 작성한 코드에서 버그가 발생했음을 있기 때문이다. 이렇게 쉽게 버그를 찾을 있게 된다.

 

테스트를 자주 수행하는 습관도 버그를 찾는 좋은 습관이다.

 

테스트를 작성하려면 부가적인 코드를 상당량 작성해야 한다. 그래서 테스트가 실제로 프르그래밍 속도를 높여주는 경험을 직접 해보지 않고서는 자가 테스트의 진가를 납득하기 어렵다.

 

테스트를 작성하기 가장 좋은 시점은 프로그래밍을 시작하기 전이다. 순서가 바뀐 같지만 아니다.

테스트를 먼저 작성하다 보면 원하는 기능을 추가하기 위해 무엇이 필요한지 고민하게 된다. 그리고 구현보다 인터페이스에 집중하게 된다는 장점도 있다.

 

이렇게 테스트부터 작성하는 습관을 바탕으로 테스트 주도 개발(Test-Driven Development:TDD) 나왔다.

TDD에선 테스트를 작성하고, 테스트를 통과하도록 코드를 작성한다. 통과하면 최대한 깔끔하게 리팩터링하는 과정을 짧은 주기로 반복한다.

 

테스트가 없는 코드를 리팩터링하게 테스트 코드부터 작성한다.

 

4.2 테스트할 샘플 코드

//샘플 데이터

export function sampleProvinceData(){

    return {

        name : "Asia" ,

        producers : [

            {name:"Byzantium",  cost:10, production:9},

            {name:"Attalia",    cost:12, production:10},

            {name:"Sinope",     cost:10, production:6},

        ],

        demand : 30,

        price : 20

    };

}

//지역 전체를 표현하는 클래스

export class Province {

    //JSON 데이터로부터 지역 정보를 읽어온다.

    constructor(doc){

        this._name = doc.name;

        this._producers = [];

        this._totalProduction = 0;

        this._demand = doc.demand;

        this._price = doc.price;

        doc.producers.forEach(d => this.addProducer(new Producer(this, d)));

    }

    addProducer(arg){

        this._producers.push(arg);

        this._totalProduction += arg.production;

    }

    get name() {return this._name;}

    get producers(){return this._producers.slice();}

    get totalProduction(){return this._totalProduction;}

    set totalProduction(arg){this._totalProduction = arg;}

    get demand(){return this._demand;}

    set demand(arg){this._demand = parseInt(arg);}

    get price(){return this._price;}

    set price(arg){this._price = parseInt(arg);}

   

    //생산 부족분

    get shortfall(){

        return this._demand - this.totalProduction;

    }

    //수익 계산

    get profit(){

        return this.demandValue - this.demandCost;

    }

    get demandValue(){

        return this.satisfiedDemand * this.price;

    }

    get satisfiedDemand(){

        return Math.min(this._demand, this.totalProduction);

    }

    get demandCost(){

        let remainingDemand = this.demand;

        let result = 0 ;

        this.producers

            .sort((a,b)=>a.cost-b.cost)

            .forEach(p=>{

                const contribution = Math.min(remainingDemand, p.production);

                remainingDemand = contribution;

                result += contribution * p.cost;

            });

        return result;

    }

}

export class Producer {

    constructor(aProvince, data){

        this._province = aProvince;

        this._cost = data.cost;

        this._name = data.name;

        this._production = data.production || 0;

    }

    get name(){return this._name;}

    get cost(){return this._cost;}

    set cost(arg){this._cost = arg;}

    get production(){return this._production;}

    set production(amountStr){

        const amount = parseInt(amountStr);

        const newProduction = Number.isNaN(amount) ? 0 : amount;

        this._province.totalProduction += newProduction - this._production;

        this._production = newProduction;

    }

}

 

4.3 번째 테스트

테스트를 위해선 테스트 프레임워크가 필요하다.

책에서는 자세히 나와 있지 않아, 인터넷에서 찾아보면서 했다.

 

VSCode 설치

https://code.visualstudio.com/download

NODE.JS 설치

https://nodejs.org/ko
mocha 테스트 프레임 워크 설치

https://mochajs.org/#installation

실행 참고

https://mochajs.org/#nodejs-native-esm-support

검증 chai 라이브러리

https://www.chaijs.com/

 

 

import {Province, sampleProvinceData} from "../sample.mjs";

import { describe } from "node:test";//Mocha 테스트 프레임워크

import assert from 'assert';

describe('province', function(){

    it('shortfall',function(){

        const asia = new Province(sampleProvinceData()); //픽스처 설정

        assert.equal(

            asia.shortfall

            ,5);//검증

    })

})

"npm test" 또는 "npm run test" 실행

 

    //생산 부족분

    get shortfall(){

        return this._demand - this.totalProduction * 2; // 실패 테스트

    }

 

성공 메시지는 너무 간결해서, 일부로 실패를 유도해 정상적으로 동작하는 확인하면 좋다.

 

 

원상 복구한다.

 

 

    //생산 부족분

    get shortfall(){

        return this._demand - this.totalProduction;

    }

 

 

 

작성 중인 코드는 최소한 간격으로 테스트하고, 하루에 번은 전체 테스트를 돌려보는게 좋다.

 

expect사용

 

describe('province', function(){

    it('shortfall',function(){

        const asia = new Province(sampleProvinceData()); //픽스처 설정

        assert.equal(asia.shortfall,5);//검증

    })

    it('shortfall2',function(){

        const asia = new Province(sampleProvinceData()); //픽스처 설정

        expect(asia.shortfall).equal(5);//검증

    })

})

 

 

테스트 코드가 하나라도 실패한다면, 리팩터링을 하면 안된다. 실패 모든 테스트를 통과한 가장 최근 체크포인트로 돌아가서 다시 작업해야 한다. (잘게 쪼개서 자주 커밋하는 것이 중요하다)

 

테스트 모든 테스트가 전부 통과했는지 빠르게 파악할 있는 방식을 찾아 사용하자

 

 

4.4 테스트 추가하기

클래스가 하는 일을 모두 살피고, 오류가 생길 있는 조건을 테스트하는 식으로 진행한다.

테스트는 위엄 요소 중심으로 작성해야 한다. 테스트 목적은 버그를 찾기 위함이다.

로직이 안들어간 게터/세터 같은 곳엔 테스트 불필요

 

가장 걱정되는 영역은 집중적으로 테스트한다. 테스트에 들어갈 노력을 집중시킨다.

 

 

수익 계산 테스트

 

describe('province', function(){

    it('shortfall',function(){

        const asia = new Province(sampleProvinceData());

        expect(asia.shortfall).equal(5);

    })

    //총 수익 계산

    it('profit', function(){

        const asia = new Province(sampleProvinceData());

        expect(asia.profit).equal(230);

    })

})

 

 

겹치는 코드를 줄여보자

 

좋은

describe('province', function(){

    const asia = new Province(sampleProvinceData()); //테스트 간 상호작용을 하는 안좋은 예시

    it('shortfall',function(){

        expect(asia.shortfall).equal(5);

    })

    //총 수익 계산

    it('profit', function(){

        expect(asia.profit).equal(230);

    })

})

 

테스트 관련 버그 가장 지저분한 유형이다. 테스트끼리 상호작용하게 하는 공유 방식

이전 테스트 결과가 이후 테스트 결과에 영향을 미친다.

 

좋은

describe('province', function(){

    //좋은 예시

    let asia;

    beforeEach(function(){

        asia = new Province(sampleProvinceData());

    })

    it('shortfall',function(){

        expect(asia.shortfall).equal(5);

    })

    it('profit', function(){

        expect(asia.profit).equal(230);

    })

})

 

방식은 테스트 마다 beforeEach구문이 실행된다. 따라서 새로운 Province객체로 독립적으로 테스트가 진행된다.

 

이렇게 한다고 지나치게 테스트가 느려지진 않는다. 만약 너무 느려진다면 공유방식을 쓰되 불변값이 되도록 확실한 검증을 해야한다. 테스트 단계에서 테스트 결과가 버그가 생겨버리면 테스트 자체를 불신하게 된다.

 

 

4.5 픽스처 수정하기

실전에서는 사용자가 값을 변경하면서 픽스처의 내용도 수정되는 경우가 흔하다.

수정 대부분은 세터에서 이뤄진다. 단순 설정 세터가 아닌 로직이 들어간 세터는 테스트해볼 필요가 있다.

 

    it('change production', function(){

        asia.producers[0].production = 20;

        expect(asia.shortfall).equal(-6);

        expect(asia.profit).equal(292);

    })

 

테스트는 가지 속성을 검증한다. 원래라면 분리해야하는 것이 맞다. 서로 밀접하게 연관되어 있어 하나의 테스트로 합친 것이다.

 

    it('change production', function(){

        asia.producers[0].production = 20;

        expect(asia.shortfall).equal(-6);

    })

    it('change production2', function(){

        asia.producers[0].production = 20;

        expect(asia.profit).equal(292);

    })

beforeEach 블록에서 "설정"하고, 테스트를 "수행"하고, 결과를 "검증"하는 패턴은 흔히 보는 패턴이다. 패턴을 "설정-실행-검증" 또는 "조건-발생-결과" 유사하게 불리는 이름이 많다.

 

beforeEach 사용하지 않으면, 단계에서 추가로 "해체" 단계가 존재한다. 픽스처를 제거하여 테스트들이 서로 영향을 주기 못하게 막는 단계다. 하지만 보통 beforeEach 사용하면 프레임 워크에서 알아서 해체해주기 때문에 무시하는 경우가 많다.

만약 생상 비용이 높아 공유해야 한다면, 단계가 필요할 것이다.

 

4.6 경계 조건 검사하기

산정 내의 범위가 아닌 경계 지점에서 발생하는 일을 미리 확인하는 것도 중요하다

 

describe('no producers', function(){

    let noProducers;

    this.beforeEach(function(){

        const data = {

            name : "No producers",

            producers : [],//비어있다.

            demand: 30,

            price: 20

        };

        noProducers = new Province(data);

    });

    it('shortfall', function(){

        expect(noProducers.shortfall).equal(30);

    });

    it('profit', function(){

        expect(noProducers.profit).equal(0);

    });

})

 

    it('zero demand', function(){

        asia.demand = 0; // 수요가없다.

        expect(asia.shortfall).equal(-25);

        expect(asia.profit).equal(0);

    })

    it('negative demand', function(){

        asia.demand = -1; // 수요가 마이너스

        expect(asia.shortfall).equal(-26);

        expect(asia.profit).equal(-10);

    })

 

수요가 음수일 수익이 음수가 나오는 것은 고객이 프로그램을 사용할 납득 못할 것이다. 음수를 세터 설정할 예외처리나 0으로 처리해야할 같다.

처럼 경계를 테스트하면 특이 상황을 처리할 단서를 얻게 된다.

 

물제가 생길 가능성이 있는 경계 조건을 생각해보고 부분을 집중적으로 테스트하자.

 

 

의미상 숫자를 받아야하지만 UI로부터 문자열을 받는다. 따라서 "" 같은 공백이 있다.

 

    it('empty string demand', function(){

        asia.demand = "";// 수요가 비어있다.

        expect(asia.shortfall).NaN;

        expect(asia.profit).NaN;

    })

 

의도적으로 코드를 망가트리는 방법을 모색한다.

 

describe('string for producers', function(){

    it('', function(){

        const data = {

            name:"String producers",

            producers: "",

            demand:30,

            price:20

        };

        const prov = new Province(data);

        expect(prov.shortfall).equal(0);

    })

})

mocha 테스트 프레임워크에선 경우 실패로 처리한다.

다른 테스트 프레임워크에선 에러와 실패를 구분하는 경우도 많다.

 

실패는 허용된 타입 안에서 예상 범위를 벗어났다는 뜻이다.

처리는 했지만, 허용 범위를 벗어남

에러는 예상치 못한 상황이 닥친 것이다.

처리조차 못함

 

이런 상황에선 어떻게 처리하는 것이 좋을까? 경우엔 외부에서 JSON입력이 들어온 것이기에 에러가 나지 않도록 처리하는 것이 좋다.

같은 코드 베이스의 모듈 에러 처리는 생각해봐야한다. 중복 유효성 검사로 오히려 문제가 있다.

 

어차피 모든 버그를 잡아낼 수는 없다고 생각하여 테스트를 작성하지 않는다면 대다수의 버그를 잡을 있는 기회를 날리는 셈이다.

 

테스트에도 수확 체감 법칙이 적용된다. 따라서 테스트는 위험한 부분이나 중요한 부분에 집중하는 좋다.

 

리팩터링 전에 테스트 스위트를 작성하지만, 리팩터링하는 동안에도 계속해서 테스트를 추가하자.

 

자가 테스트 코드는 리팩터링 개발자에게 코드가 정상동작할 것이라는 안도감을 준다.

 

4.7 끝나지 않은 여정

테스트로 리팩터링 못지 않게 중요하다. 리팩터링에 필요한 토대일 뿐만 아니라, 자체로도 프로그래밍에 중요한 역할을 한다.

 

장에서 보여준 테스트는 단위 테스트다.

단위 테스트는 코드의 작은 영역만 대상으로 빠르게 실행되도록 셀계된 테스트를 의미한다.

 

번에 완벽한 테스트를 만들 수는 없으므로 지속적으로 테스트 스위트도 보강을 해야 한다.

 

특히, 버그를 발견하는 즉시 발견한 버그를 명확히 잡아내는 테스트부터 작성하는 습관을 들여야 한다.

 

새로운 기능을 추가하기 전에 테스트부터 작성한다.

 

테스트를 충분히 헀는지 확인하려는 지표로 테스트 커버리지를 많이 언급한다. 하지만 테스트 커버리지 분석은 말그대로 코드에서 테스트하지 않은 영역을 찾는 것이지, 테스트 스위트 품질과는 크게 상관 없다.

 

테스트 스위트가 충분한지는 정량적으로 측정할 없으므로, 주관적인 영역이다. 리팩터링 과정에서 테스트 스위트가 전부 초록불일 , 리팩터링 과정에서 버그가 없다고 확신이 들면 좋은 테스트 스위트다.

 

 

 

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

리팩터링 적용 방법을 아는 것과 제때 적용할 아는 것은 다르다.

리팩터링을 언제 시작하고 언제 그만할지 판단하는 일은 리팩터링 작동 원리를 아는 못지않게 중요하다

다만 이런 기준을 제기할 수는 없다. 프로젝트마다 다들 것이기 때문이다.

장에선 리팩터링 대상을 찾는 방법을 알려준다.

 

3.1 기이한 이름

코드는 단순하고 명료한 것이 최선이다.

누가 이름만 보고도 무슨 일을 하고, 어떻게 사용해야 하는지 있어야 좋은 이름이다.

 

이름은 IDE 쉽게 바꿀 있으므로 좋은 이름이 생각나면 주저하지 말고 바꾸자

 

이름 바꾸는 것은 이름을 다르게 표현하는 것이 아니다. 적절한 이름이 떠오르지 않는다면 설계에 근본적인 문제가 있을 가능성이 높다.

 

이름을 정리하면 코드가 간결해져 문맥 파악에 도움된다.

 

3.2 중복 코드

똑같은 구조 코드가 여러 곳에 반복된다면, 코드들의 차이점이 없는지 주의깊게 봐야한다.

또한 하나가 변경사항이 생기면 다른 모든 코드도 살펴봐야한다.

 

예시

클래스에서 메서드가 동일한 표현식을 쓴다.

이땐 함수 추출하기로 메서드가 추출된 메서드를 호출하게 바꾼다.

 

메서드 코드가 비슷한 경우, 문장 슬라이드하기로 비슷한 부분을 곳에 모아 함수 추출하기를 적용할 있는지 살펴본다.

 

같은 부모로부터 파생된 서브 클래스들에 코드 중복이 있다면, 메서드 올리기를 적용해 부모로 옮긴다.

 

3.3 함수

오랜 기간 활용되는 프로그램들은 짧은 함수로 구성된 경우가 많다.

짧은 함수는 코드가 끝없이 위임하는 방식으로 작성된다.

함수를 짧게 구성할 , 코드를 이해하고 공유하고 선택하기 쉬워진다.

 

예전 언어는 호출 비용이 커서 짧은 함수를 피했다.

요즘 언어는 프로세스 안에서의 함수 호출 비용이 거의 없다.

 

짧은 함수로 구성된 코드를 이해하기 쉽게 만드려면 좋은 이름을 지어야 한다.

좋은 이름을 가진 함수는 본문을 필요가 없다.

이러기 위해서 적극적으로 함수를 쪼개야 한다.

주석을 달아야 코드는 함수로 만든다.

원래 코드보다 길어지더라도 함수로 만든다.

 

함수 이름을 지을 , 동작방식이 아닌 함수가 무엇을 하는지에 대한 '의도' 표현해야한다.

 

함수를 짧게 만드는 작업은 거의 합수 추출하기로 한다.

 

매개변수와 임시 변수가 많으면 함수 추출하기 힘들다. 이런 상황에서 함수 추출 똑같이 추출된 함수로 매개변수가 많아진다.

 

임시 변수를 질의 함수로 바꾸기로 임시 변수 수를 줄일 있다.

매개변수 객체 만들기와 객체 통째로 넘기기로 매개변수 수를 줄일 있다.

 

추출할 코드를 찾는 방법으로 주석 살펴보기가 있다. 보통 주석은 이해하기 난해한 곳에 붙어있기 때문이다.

 

조건문과 반복문도 추출 대상 후보다.

조건문은 조건문 분해하기를 적용한다.

switch문은 case 마다 함수 추출하기를 적용한다.

같은 조건을 기준으로 나뉘는 switch문은 조건부 로직을 다형성으로 바꾸기를 적용한다.

 

반복문도 코드를 추출해 함수를 만든다. 이때 함수에 적합한 이름이 떠오르지 않는다면, 함수는 가지 이상 작업이 섞여 있을 가능성이 있다. 이땐 반복문 쪼개기를 적용해 작업을 분리한다.

 

3.4 매개변수 목록

매개변수 목록이 길어지면 자체로 이해하기 어려워진다.

 

다른 매개변수에서 값을 얻어올 있는 매개변수는 매개변수를 질의 함수로 바꾸기로 제거할 있다.

사용 중인 데이터 구조에서 값들을 뽑아 각각 별개 매개변수로 전달하는 코드는 객체 통째로 넘기기를 적용해서 원본 데이터 구조를 그대로 넘긴다.

항상 함께 전달되는 매개변수들은 매개변수 객체 만들기로 하나로 묶어버린다.

함수 동작방식을 정하는 플래그 매개변수는 플래그 인수 제거하기로 없앤다.

 

클래스는 매개변수 목록을 줄이는 효과적인 수단이다.

여러 함수 특정 매개변수들의 값을 공통으로 사용할 여러 함수를 클래스로 묶기를 적용해 공통 값들을 클래스의 필드로 정의한다.

 

3.5 전역 데이터

가장 악취가 지독한 축에 속한다.

 

전역 데이터의 문제는 코드 베이스 어디에서든 건드릴 있기 때문에 누가 값을 바꿨는지 찾아낼 메커니즘이 없다.

전역 데이터의 대표적인 형태는 전역 변수지만 클래스 변수와 싱글톤도 같은 문제를 공유한다.

 

이를 방지하기 위해 변수 캡슐화하기를 적용한다.

데이터를 함수로 감싸는 것만으로도 데이터 수정하는 부분을 쉽게 찾을 있고, 접근 제한을 있다. 나아가 접근자 함수들을 클래스나 모듈에 넣고, 안에서만 사용할 있도록 접근 범위를 최소로 제한할 있다.

 

3.6 가변 데이터

코드의 다른 곳에서 다른 값을 기대한다는 사실을 모르고 수정해버리면 오작동하게 된다.

문제가 드문 조건에서만 발생한다면 원인은 찾기 힘들다.

이런 문제로 함수형 프로그래밍에서는 데이터는 변하지 않고, 데이터 변경 복사본을 사용하여 반환한다.

 

데이터 수정에 따른 위험을 줄이는 방법

변수 캡슐화하기를 적용해 함수를 통해서만 값을 수정하도록 감시

하나의 변수 용도가 다른 값들을 저장하느라 값을 갱신한다면 변수 쪼개기를 이용하여 용도별로 독립 변수에 저장하여 갱신 발생 여지를 없앤다. 그리고 갱신로직은 다른 코드와 떨어트린다.

API 만들 때는 질의 함수와 변경 함수 분리하기를 적용해 필요한 경우에만 사이드이펙트가 있는 함수를 호출하도록 한다.

가능한 세터 제거하기

 

값을 다른 곳에서 설정할 있는 가변 데이터는 특히 위험하다. 경우 파생 변수를 질의 함수로 바꾸기를 적용한다.

 

유효범위가 좁다면 가변 데이터라도 문제를 일으키지 않는다. 하지만 나중에 유효범위가 넓어질 있기 때문에 여러 함수를 클래스로 묶기나 여러 함수를 변환 함수로 묶기를 활용해 변수를 갱신하는 코드들의 유효범위를 제한한다.

구조체처럼 내부 필드에 데이터를 담고 있는 변수는 참조를 값으로 바꾸기를 적용하여, 내부 필드를 직접 수정하지 말고 구조체를 통째로 교체하는 것이 좋다.

 

 

3.7 뒤엉킨 변경

개발자는 코드를 수정할 시스템에서 고쳐야 군데를 찾아서 부분만을 수정하길 바란다. 이것이 불가능하다면 뒤엉킨 변경과 산탄총 수술 하나이다.

 

뒤엉킨 변경은 단일 책임 원칙이 제대로 지켜지지 않을 나타난다.

하나의 모듈이 서로 다른 이유들로 인해 여러 가지 방식으로 변경되는 일이 많을 발생한다.

예시로 DB추가 함수 개를 바꿔야 한다면 뒤엉킨 변경이 발생한 것이다.

 

순차적으로 실행되는게 자연스러운 맥락이라면, 다음 맥락에 필요한 데이터를 특정한 데이터 구조에 담아 전달하게 하는 식으로 단계 분리한다.

전체 처리 과정 곳곳에서 각기 다른 맥락의 함수를 호출하는 빈도가 높다면, 맥락에 해당하는 적당한 모듈들을 만들어서 관련 함수들을 모은다.

 

 

 

3.8 산탄총 수술

뒤엉킨 변경과 비슷하면서도 정반대

코드가 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 ,

변경할 부분이 퍼져 있다면, 수정을 못할 확률이 증가한다.

 

이럴 변경 대상들을 함수 옮기기와 필드 옮기기로 모두 모듈에 묶어두면 좋다.

비슷한 데이터를 다루는 함수가 많으면 여러 함수를 클래스로 묶기를 적용한다

데이터 구조를 변환하거나 보강하는 함수들에는 여러 함수를 변환 함수로 묶기를 적용한다.

 

어설프게 분리된 로직을 함수 인라인하기나 클래스 인라인하기 같은 인라인 리팩터링으로 하나로 함치는 것도 하나의 방법이다.

메서드나 클래스가 비대해지지만, 인라인류는 나중에 추출하기 리팩터링이 용이해진다.

 

3.9 기능 편애

프로그램 모듈화할 코드를 여러 영역으로 나눈 같은 영역 내에서 상호작용은 늘리고, 영역 사이 상호작용은 줄이는 주력한다.

 

기능편애는 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용 일이 많을 풍기는 냄새다.

 

대게 함수 옮기기나 함수 추출하기로 옮기면 된다.

함수가 사용하는 모듈이 다양해 어디로 옮길지 명확하지 않을 때는 가장 많은 데이터를 포함한 모듈로 옮긴다. 그리고 함수 추출하기로 함수를 여러 조각으로 나눈 적합한 모듈로 옮긴다.

 

전략 패턴과 방문자 패턴은 뒤엉킨 변경 냄새를 없앨 활용하는 패턴이다.

함께 변경할 대상을 한데 모은다.

 

 

3.10 데이터 뭉치

데이터 항목이 여러 곳에서 함상 함께 뭉쳐 다니는 것을 말한다.

 

경우 클래스 추출하기로 하나의 객체로 묶는다. 그리고 매개변수 객체 만들기나 객체 통째로 넘기기로 매개변수 수를 줄인다.

매개변수로 넘긴 클래스가 함수 내에서 데이터를 전부 사용하지 않아도 된다. 중요한 것은 전보다 함수 선언부가 깔끔해진다는

 

데이터 뭉치인지 판별하는 방법은 데이터 뭉치 하나만 지워봤을 나머지 데이터만으로는 의미가 없다면, 데이터 뭉치이다.

 

3.11 기본형 집착

자신에게 주어진 문제에 맞는 기초 타입을 직접 정의하기를 꺼리는 것을 말한다.

주로 문자열을 다룰 흔히 나타난다.

전화번호를 예를 들면, 단순히 문자열로만 표현하기엔 부족하다. 사용자에게 보여줄 일관된 형식으로 출력해주는 기능이라도 있어야한다.

 

기본형을 객체로 바꾸기를 적용해 의미있는 자료형으로 변경하는게 좋다.

 

3.12 반복되는 switch

중복된 switch문이 문제가 되는 이유는 조건절을 하나 추가할 때마다 다른 switch문들도 모두 찾아서 함께 수정해야 하기 때문이다. 이럴 다형성은 반복된 switch문의 문제를 효과적으로 해결해준다.

 

3.13 반복문

과거 언어들은 반복문에 대안을 제시하지 못했다. 현재 언어들은 일급 함수를 지원하는 경우가 많아 반복문을 파이프라인으로 바꾸기를 적용해 반복문을 제거할 있다.

 

3.14 성의없는 요소

프로그래밍 언어가 제공하는 함수, 클래스, 인터페이스 코드 구조를 잡는데 활용하는 것이 요소다.

 

성의없는 요쇼로는 본문 코드를 그대로 쓰는 것과 다름없는 함수, 실질적인 메서드는 하나뿐인 클래스 등이 있다.

 

함수 인라인하기나 클래스 인라인하기, 상속을 사용했다면 계층 합치기로 제거한다.

 

3.15 추측성 일반화

나중에 필요할 거라는 생각으로 만든 코드를 말한다. 당장은 필요 없는 후킹(hooking)포인트와 특이 케이스 처리 로직을 작성해둔 코드에서 나타만다.

 

하는 일이 거의 없는 추상 클래스는 계층 합치기로 제거

쓸데없이 위임하는 코드는 함수 인라인하기나 클래스 인라인하기로 제거

본문에서 사용되지 않는 매개변수는 함수 선언 바꾸기로 제거

 

추측성 일반화 코드는 테스트 코드에서만 사용하는 함수나 클래스에서 흔히 있다.

 

 

3.16 임시 필드

임시 필드를 갖도록 작성하면 코드를 이해하기 어렵다. 사용자는 쓰이지 않는 것처럼 보이는 필드가 존재하는 이유를 파악하느라 골치 아프다.

 

이런 필드들을 발견하면 클래스 추출하기로 옮긴다. 그리고 함수 옮기기로 임시 필드들과 관련된 코드를 클래스로 옮긴다.

 

3.17 메시지 체인

클라이언트가 객체를 통해 다른 객체를 얻은 방금 얻은 객체에 다른 객체를 요청하는 식으로, 다른 객체를 요청하는 작업이 연쇄적으로 이어지는 코드를 말한다.

이럴 경우 중간 단계를 수정하면 클라이언트 코드도 수정해야 한다.

 

문제는 위임 숨기기로 해결할 있다.

래픽터링은 여러 지점에 사용할 있지만, 이러면 모두 중개자가 돼버린다.

 

3.18 중개자

객체의 대표적인 기능 하나로, 외부로부터 세부사항을 숨겨주는 캡슐화가 있다.

캡슐화하는 과정에서 위임이 자주 활용된다.

 

위임이 지나치면 문제가 된다. 클래스 메서드 대부분이 다른 클래스에 구현을 위임하고 있다면, 중개자 제거하기를 통해 실제로 일하는 객체와 직접 소통하도록 만든다.

 

3.19 내부자 거래

모듈 사이의 거래가 많으면 결합도가 높아진다고 한다. 따라서 빈도를 낮추는 것이 좋다.

 

데이터를 주고 받는 모듈들이 있다면 함수 옮기기와 필드 옮기기로 떼어놓아서 사적으로 처리하는 부분을 줄인다.

여러 모듈이 같은 관심사를 공유한다면 공통 부분을 정식으로 처리하는 3 모듈을 새로 만들거나 위임 숨기기를 이용해 다른 모듈이 중간자 역할을 하게 한다.

 

3.20 거대한 클래스

클래스가 너무 많은 일을 하면 필드 수가 늘어나고 클래스에 필드가 많으면 중복 코드가 생기기 쉽다.

 

클래스 추출하기로 유사한 필드들 일부를 따로 묶는다.

일반적으로 접두나 접미가 같은 필드들이 좋은 후보군이 된다.

분리할 컴포넌트를 원래 클래스의 상속관계로 만들고 싶다면 슈퍼클래스 추출하기나 타입 코드를 서브클래스로 바꾸기를 적용한다.

 

코드량이 많은 클래스도 중복 코드와 혼동을 을으킬 여지가 크다. 이럴 클래스 내부에서 중복을 제거한다.

 

클라이언트들이 거대 클래스를 이용하는지 패턴을 파악하여 클래스를 쪼갤지 단서를 얻을 수도 있다. 클라이언트들이 거대 클래스의 특정 기능 그룹만 주로 사용하면, 기능 그룹을 여러 클래스로 분리한다.

 

3.21 서로 다른 인터페이스의 대안 클래스들

클래스를 사용할 장점은 필요에 따라 다른 클래스로 교체할 있다는 점이다

교체하려면 인터페이스가 같아야 한다. 따라서 함수 선언 바꾸기로 메서드 시그니처를 일치시킨다. 이것만으로 부족하면. 함수 옮기기를 이용해 인터페이스가 같아질 때까지 필요한 동작들을 클래스 안으로 넣는다. 이과정에서 클래스들 사이 중복 코드가 생기면 슈퍼클래스 추출하기를 사용한다.

 

3.22 데이터 클래스

데이터 클래스는 데이터 필드와 그에 따른 게터/세터만 있는 클래스

단순히 데이터 저장 용도로만 쓰인다. 때문에 함부로 다를 때가 많다.

public 필드가 있다면 레코드 캡슐화하기로 숨긴다. 변경하면 안되는 필드는 세터를 제거한다.

 

게터나 세터를 사용하는 메서드를 찾아서 함수 옮기기로 데이터 클래스로 옮길 있는지 확인한다.

메서드를 옮기기 힘들다면 함수 추출하기로 옮길 있는 부분만 별도 메서드로 뽑아낸다.

 

3.23 상속 포기

서브클래스에서 부모로부터 상속받기 싫은 경우

 

같은 계층에 서브클래스를 하나 새로 만들고, 메서드 내리기와 필드 내리기를 활용해서 물려받지 않을 부모 코드를 새로 만든 서브클래스로 넘긴다. 그러면 부모 클래스에는 공통부분만 남는다

 

현재는 방식을 권하지 않는다. 일부 동작을 재활용하기 위해 상속을 사용하기도 하는데, 실무에선 유용하기 때문이다. 냄새가 나긴해도 심한 정도가 아니다.

 

상속 포기 냄새는 서브클래스가 부모의 동작은 필요로 하지만 인터페이스는 따르고 싶지 않을 심하게 난다. 인터페이스를 따르지 않을 거면, 아예 상속 메커니즘에서 빼야한다.

 

3.24 주석

주석이 장황할 수록 코드를 잘못 작성했기 때문인 경우가 많다.

 

주석을 남겨야할 같으면 먼저 함수를 리팩터링해본다. 그래도 필요하다 싶으면 남긴다.

 

 

 

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

 

2.5 리팩터링 고려할 문제

손익을 제대로 이해하고 있어야 한다.

 

기능 개발 속도 저하

리팩터링의 궁극적인 목표는 개발 속도를 높여서, 적은 노력으로 많은 가치를 창출하는 것이다.

 

기능을 구현하기 편해지겠다 싶은 리팩터링이라면 주저하지 않고 리팩터링부터 한다.

 

직접 건드릴 일이 거의 없거나, 불편한 정도가 심하지 않으면 하지 않는

개선할 방향이 마땅히 떠오르지 않으면 일단 둔다.

 

보통은 리팩터링을 너무 안해서 문제

 

코드베이스가 건강하면 기존 코드를 새로운 방식으로 조합하기 쉬워서 복잡한 기능을 빨리 추가할 있다.

 

리팩터링의 본질은 코드베이스를 예쁘게 꾸미는데 있지 않다. 오로지 경제적인 이유로 하는 것이다.

 

 

코드 소유권

리팩터링 대상 코드가 다른 곳에서 호출하는 경우, 또는 API 경우 누가 얼마나 쓰고있는지 모른다.

기존 코드를 두고 기존 코드가 다른 함수를 호출하는 식으로 리팩터링해도 되지만, 그러면 인터페이스가 지져분해진다.

 

이를 방지하기 위해선 조직 시스템을 손봐야한다.

단위로 코드 소유권을 공유하여, 코드를 호출하는 팀원 코드(클라이언트) 수정할 있게 하는것, 그리고 수정한 팀원 코드를 커밋 요청을 코드 주인인 팀원에게 하는 것이다.

 

 

브랜치

가장 흔한 단위 작업 방식 기능 브랜치 방식

팀원 마다 브랜치를 가진다.

어느 정도 작업을 하면, 마스터 브랜치로 통합한다.

방식은 프로덕션 버전으로 릴리스할 마스터에 통합하는 경우가 많다.

이렇게 되면 독립 브랜치로 작업하는 기간이 길어져 마스터로 통합하기 어려워지는 경우가 발생할 있다.

이를 방지하려면 수시로 개인 브랜치를 마스터로 리베이스 또는 머지를 해야한다.

 

여기서 말하는 머지와 통합 차이

머지는 개인 브랜치로 마스터를 MERGE 하는 (마스터 =단방향=> 브랜치)

통합은 머지 전에 마스터로부터 PULL 마스터로 PUSH 하는 (마스터 <=양방향=> 브랜치)

 

 

통합 인터벌이 길면 발생할 있는 흔한 문제는 다음과 같다.

누군가 개인 브랜치에서 작업한 내용을 마스터에 통합하기 전까지 다름 팀원은 내용을 모른다.

내가 함수 이름을 변경했고, 마스터로 통합을 늦게 했다.

사이 팀원은 이름이 바뀔지 모르기 때문에 함수를 호출하는 코드를 개발했다.

그리고 내가 마스터로 통합을 하면, 팀원 코드에서 버그가 발생한다.

 

처럼 통합주기가 길면 길수록 머지 복잡도가 급격히 높아진다.

이를 방지하기 위해 주기(최소 하루 한번) 짧게 가져가는 방식을 지속적 통합(Continuous Integration:CI) 또는 트렁크 기반 개발(Trunk-Based Development:TBD)이라 한다.

 

CI 위해선 마스터를 건강하게 유지하도록 거대한 기능을 잘게 쪼개는 방법과 기능을 키고 있는 기능 토글을 적용할 알아야 한다.

 

CI 리팩터링과 궁합이 좋다. 기법을 합친 것이 익스트림 프로그래밍(eXtreme Programming:XP)이다.

 

기능별 브랜치 방식도 통합 주기를 짧게 가져가면 문제를 최소화 있다.

상황에 맞게 정책을 적용해 사용하면 된다.

 

 

 

테스팅

리팩터링 특징은 겉보기 동작은 똑같이 유지된다는 것이다.

 

리팩터링은 단계별 변경 폭이 작아 도중에 발생한 버그를 쉽게 잡을 있다.

만일 원인을 찾더라도 버전 관리 시스템을 이용하여 가장 최근에 정상 작동하던 상태로 돌리면 그만이다.

 

핵심은 오류를 재빨리 잡는 것이다.

이렇게 하려면 코드의 다양한 측면을 검사하는 테스트 스위트(자가 테스트 코드) 필요하다.

 

자가 테스트 코드는 기능 추가를 안전하게 해준다.

조금에 변경에도 계속 테스트를 한다면, 문제를 일으킨 부분이 순식간에 들어난다.

또한 리팩터링 과정에서 버그가 생길 있다는 불안을 해소할 있다

 

테스트 코드 없이 리팩터링하는 방법도 있다.

테스트 커버리지가 좁은 범위에선 안전하다고 검증된 가지 리팩터링 기법만을 사용해도 효과적으로 리팩터링 있다.

 

 

레거시 코드

레거시 코드의 문제, 대체로 복잡하고 테스트도 제대로 갖춰지지 않은 것이 많다.

무엇보다 다른 사람이 작성한 것이라 거부감이 든다.

 

레거시 시스템을 파악할 리팩터링이 도움된다.

레거시 시스템은 테스트 코드가 없어 리팩터링하기 까다롭다.

 

이를 위해 테스트를 보강해야하는 이것도 상당히 까다롭다.

문제는 마땅히 해결할 방법이 없다. 그래서 처음부터 자가 테스트 코드를 만들어야 한다.

 

캠핑 규칙을 상기하며, 부분 별로 조금씩 나누어 정복하는 것이 최선이다.

 

 

2.6 리팩터링, 아키텍처, 애그니(YAGNI)

기존 방식, 코딩 전에 설계와 아키텍처를 완성하고 코딩 시작

코딩 전에 아키텍처를 확정지으려 문제는 소프트웨어 요구사항을 사전에 모두 파악해야 한다는 것이다. 사실상 실현 불가능한 목표다.

 

리팩터링은 이런 관점을 바꿔 코딩 시작 후에도 요구사항 변화에 따른 대응하도록 코드 베이스를 설계해준다.

 

향후 변경에 유연하도록 유연성 메커니즘을 소프트웨어에 심지도 한다.

예를 들어, 나중에 사용할 같은 매개변수를 미리 적용해 두는

이것도 결국 예측에 불과하기에 매개변수가 늘어난다던가 변수가 생기면 오히려 변화에 대응하는 능력을 떨어뜨를 때가 대부분이다.

 

유연성 매커니즘이란 예상되는 결과를 미리 예측하고 대비해두는

 

리팩터링은 앞으로 어느 부분에 유연성이 필요하고 어떻게 해야 변화에 가장 대응할지 추측하지 않고 현재 요구사항에 최선에 소프트웨어를 만든다.

진행하면서 사용자 요구사항을 이해하게 되면 아키텍처도 그에 맞게 리팩터링한다.

필요하다고 확신이 때만 유연성 메커니즘을 미리 추가한다.

 

이런 설계 방식을 간결한 설계, 점진적 설계, YAGNI(you aren't going to need it)등으로 부른다.

YAGNI 아키텍처를 고려하지 말라는 뜻이 아니라 아키텍처와 설계를 개발 프로세스에 녹이는 방식이다.

 

YAGNI 적용이 선제적인 아키텍처에 소홀하라는 뜻이 아니다.

확실한 부분은 미리 설계해두고, 나중에 이해도가 높아졌을 나머지를 리팩터링으로 설계한다.(진화형 아키렉처 원칙)

 

 

2.7 리팩터링과 소프트웨어 개발 프로세스

익스트림 프로그래밍은 지속적 통합, 자가 테스트 코드, 리팩터링, 등을 하나로 묶은 프로세스다.

 

자가 테스트 코드 + 리팩터링 = 테스트 주도 개발(Test-Driven Development:TDD)

 

익스트림 프로그래밍은 최초의 애자일 소프트웨어 방법론 하나다.

애자일을 제대로 적용하려면 리팩터링에 대한 팀의 역량과 열정이 뒷받침되어 프로세스 전반에 리팩터링이 자연스럽게 스며들도록 해야 한다.

 

리팩터링의 번째 토대는 자가 테스트 코드다.

 

팀으로 개발하면서 리팩터링 하려면 팀원이 다른 팀원의 작업을 방해하지 않으면서 리팩터링을 있어야 한다.

이를 위해선 지속적 통합이 필요하다. 그래야 리팩터링으로 인한 코드 변화를 빠르게 알아내지 못해 생기는 문제를 방지할 있다.

함수 이름 변경, 함수 제거 등을 한다면, 잦은 통합 말고 호출부에서 감지할 방법이 마땅치 않다.

이런 문제를 빠르게 발견할 있게 돕는 자가 테스크 코드는 지속적 통합의 핵심 요소다.

 

지속적 통합, 자가 테스트 코드, 리팩터링 기법을 적용하면 YAGNI 설계 방식으로 개발을 진행 있다.

추측에 근거한 유연성 메커니즘을 갖춘 시스템보다 단순한 시스템이 변경하기 훨씬 쉽다.

 

위와 같은 토대를 마련했다면, 애자일이 주는 이점인 지속적인 배포를 수행할 있다.

이로 인해 고객의 비즈니스 요구를 프로덕션 코드로 빠르게 반영해 빠르게 배포할 있다.

 

 

 

2.8 리팩터링과 성능

직관적인 설계 vs 성능

 

리팩터링이 성능을 저하시킬까봐 걱정하는 사람이 많다.

저자는 리팩터링이 성능이 느려진다고 해도 진행한다. 이유는 소프트웨어를 이해하기 쉽게 만들기 때문이다.

 

성능을 무시하기 때문이아니라 경험상 성능이 낮아지는 경우가 드물기 때문이다.

만약 느려진다고 해도 리팩터링된 코드는 되려 튜닝하기 좋은 상태이다.

 

빠른 소프트웨어 작성 방법 가지

예산 분배 방식

가장 엄격한 방식, 설계를 여러 컴포넌트로 나눠 각각 시스템 자원(시간,공간) 예산을 할당

컴포넌트는 할당된 예산만 사용 가능, 컴포넌트간 예산 교환 가능

 

지속적인 관심 기울이기

가장 흔한 방식이지만 효과는 좋지 않다.

성능 개선을 위해 수정하다보니 코드만 다루기 어렵게 변한다.

방식은 최적화가 프로그램 전체에 퍼지게 된다.

 

일부분만 최적화

대부분 프로그램은 전체 코드 극히 일부에서 시간을 소비한다. 그래서 코드 전체를 고르게 최적화 한다면, 대부분이 낭비인 것이다.

최적화는 '90% 시간은 낭비'라는 통계에서 착안한 것이다.

 

성능 최적화는 신경 안쓰고, 코드를 다루기 쉽게 만드는 데만 집중하며 개발한다.

성능 최적화 단계에선 프로파일러 프로그램으로 시간과 공간을 잡아먹는 지점을 탐색한다.

특정된 부분들만 개선한다.

성능이 개선되지 않았다면, 이전 개선 내용을 되돌리고, 다른 최적화 대상을 개선한다.

과정을 목표 성능이 도달할때까지 반복한다.

 

이렇게 되면 필요한 부분만 개선하게 된다.

 

리팩터링해두면, 튜닝에 소비할 시간을 있다. 또한 코드 범위가 줄어, 프로그램으로 성능을 세밀하게 측정할 있다. 때문에 튜닝이 쉬워진다.

 

결과적으로 리팩터링은 단기적으로는 성능이 떨어질 수도 있으나 최적화 단계에선 튜닝하기가 쉬워져 빠른 소프트웨어를 얻게 된다.

 

2.9 리팩터링의 유래

명확한 유래는 없다.

현재 리팩터링이란 자체는 중요성이 충분히 검증됐고, 증거로 많은 IDE에서 자동 리팩터링 기능까지 지원한다.

 

 

2.10 리팩터링 자동화

과거 리팩터링과 현재 리팩터링에서 가장 변화는 자동 리팩터링 지원 도구 등장이다.

IDE에서 지원하는 자동 리팩터링

 

리팩터링을 자동화하는 가장 수준 낮은 방식은 텍스트로 조작하는 것이다.

불완전하기에 테스트해보기 전까지 신뢰해선 안된다.

텍스트 에디터가 해당한다.

 

코드를 텍스트가 아닌 구문 트리로 인식해 자동 리팩터링하는 것이 제대로된 방식이다.

IDE 해당한다.

 

언어마다 자동 리팩터링을 지원하는 정도가 달라진다.

자바와 같은 정적 타입 언어는 안전하게 리팩터링을 지원할 있다.

자바는 클래스 이름이 조금만 달라도 다른 타입이다.

메서드 이름 변경 같은 메서드 이름이라도 타입에 맞게 정확히 걸러낸다.

 

자동 리팩터링 기능은 IDE 강력하므로 리팩터링은 IDE 진행하는 것이 좋다.

 

 

 

 

2.11 알고 싶다면

리팩터링이 궁금하다, 월리엄 웨이크의 "리팩터링 워크북"

패턴과 접목된 리팩터링, 조슈아 케리에프스키의 "패턴을 활용한 리팩터링"

레거시 코드를 리팩터링하는 방법, 마이클 페더스의 "레거시 코드 활용 전략"

 

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

2.1 리팩터링 정의

소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법

 

올바른 리팩터링은 리팩터링이 중간에 멈춰도 항상 동작 보장한다.

 

번에 바꿀 있는 작업을 수많은 단계로 잘게 나눠서 작업하는 모습이 비효율적이라고 생각할 있다.

하지만 이렇게 잘게 나눔으로써, 단계들이 체계적으로 구성되며

무엇보다 디버깅하는 시간이 줄어든다.

 

리팩터링 전 후 코드는 똑같이 동작해야 한다.

 

리팩터링과 성능 최적화 차이

코드를 변경하지만 프로그램 전반적인 기능은 유지한다.

목적이 다르다.

 

리팩터링은 코드를 이해하고 수정하기 쉽게 만드는 것이다.

프로그램 성능은 좋아질 수도 아빠질 수도 있다.

 

성능 최적화는 속도 개선에만 신경 쓴다.

코드는 다루기 어렵게 바뀔 수도 있다.

 

 

2.2 두 개의 모자

소프트웨어 개발 목적이 "기능 추가"인지 "리팩터링"인지 명확히 해야한다.

 

"기능 추가"모자를 썼다면, 절대 기존 코드를 건들지 않고, 기능 추가만한다.

진척도는 테스트를 추가해 통과하는지

"리팩터링"모자를 썼다면, 기존 코드만 재구성하고, 절대 기능 추가는 하지 않는다.

기능 추가 모자를 썼을 테스트를 누락한게 아니라면, 테스트도 추가하지 않는다.

 

물론 소프트웨어 개발 모자를 번갈아가며 있다.

 

 

2.3 리팩터링하는 이유

리팩터링하면 소프트웨어 설계가 좋아진다

리팩터링하지 않으면 소프트웨어 설계(아키텍처) 썩기 쉽다.

 

어키렉터가 무너짐 => 코드만 봐서 설계 파악 어려움

설계 유지보수가 어려워짐

 

설계가 나쁘면 코드가 길어진다.

같은 코드가 여러 곳에 나타날 있기 때문이다.

그래서 중복 코드 제거는 설계 개선 작업의 중요한 축을 차지한다.

 

코드량이 준다고 성능이 좋아지진 않는다. 하지만 코드량이 줄면 수정에 드는 노력이 감소한다.

코드가 수록 실수 없이 수정하기 힘들어 진다.

 

코드 중복은 코드 부분만 살짝 바꿔서는 시스템이 예상대로 작동하지 않을 있다.

 

 

리팩터링하면 소프트웨어를 이해하기 쉬워진다

컴퓨터에게 시키려는 일과 이를 표현한 코드의 차이를 최대한 줄여야 한다.

 

소스 코드를 컴퓨터만 사용하는게 아니다, 나중에 누군가 코드를 수정하고자 일게 있다.

 

프로그래밍에서는 사람이 가장 중요하지만 이를 소홀하기 쉽다.

컴파일 시간이 몇초 걸리는 보다. 다른 프로그래머가 코드를 제대로 이해하도록 코드 수정 시간을 줄이는 것이 유익하다.

 

리팩터링은 코드를 읽히게 한다.

 

리팩터링은 다른 사람은 배려하기 위해서가 아니다. 사실 다른 사람이 바로 자신일 때가 많다.

 

 

 

리팩터링하면 버그를 쉽게 찾을 있다

코드를 이해하기 쉬우면 버그를 찾기 쉽다.

리팩터링하면 코드가 하는 일을 깊이 파악하게 되면서, 버그를 찾기 수월해진다.

 

 

리팩터링하면 프로그래밍 속도를 높일 있다

보통 리팩터링에 대해 설명하면 품질을 높인다는 것엔 이견이 없다.

내부 설계와 가독성 개선이 버그를 주어들게 하고 이는 모두 품질 향상에 직결된다.

하지만 리팩터링하는 시간이 드니 전체 개발 속도는 떨어질까봐 걱정할 수도 있다.

 

보통 시스템을 오래 개발 중인 개발자들과 얘기하다 보면 초기에는 진척이 빨랐지만 현재는 새기능을 하나 추가하는 훨씬 오래 걸린다는 말을 많이 한다.

기존 코드에 코드를 추가하기도 어렵고, 과정에서 버그는 덤이다.

기존 코드에 패치에 패치를 거듭하면서 프로그램 동작을 파악하기 어려워진다.

종국엔 새로 개발하는게 빠르겠다는 생각을 하게 된다.

 

내부 설계가 소프트웨어는 새로운 기능을 추가할 지점과 어떻게 고칠지를 쉽게 찾을 있다.

 

모듈화가 되어 있으면 전체 코드 베이스 작은 일부만 이해하면 된다.

 

 

2.4 언제 리팩터링해야 할까?

프로그래밍을 하는 도중에 주기적으로 하는 것이 좋다. 

즉, 별도 시간을 마련하지 않고, 일상 코딩에 리팩터링을 녹여내는 것

3의 법칙

  1. 처음에는 그냥 한다.
  2. 비슷한 일을 번째로 하게 되면, 일단 계속 진행한다.
  3. 비슷한 일을 번째 하게 되면 리팩터링한다.

준비를 위한 리팩터링: 기능을 쉽게 추가하게 만들기

리팩터링하기 가장 좋은 시점은 코드베이스에 기능을 새로 추가하기 직전이다.

 

현재 코드를 살펴보며, 구조를 살짝 바꾸면 다른 작업하기 쉬워질 부분을 찾는다.

예를 들어, 리터럴 개가 방해되는 함수가 있다고 하자.

함수를 복제해서 값만 수정해서 써도 되지만, 이러면 나중에 변경이 생길 복제해서 코드 전부를 수정해야 한다. 심지어 여기저기 숨어있기까지 하다.

 

이해를 위한 리팩터링: 코드를 이해하기 쉽게 만들기

코드 수정을 위해선 먼저 코드가 하는 파악이 우선이다.

코드를 파악할 때마다 코드의 의도가 명확하게 드러나도록 리팩터링할 여지가 있는지 찾아보고 손본다.

조건부 로직 구조가 이상하지 않은지, 함수 이름이 이상한지

 

과정에서 코드를 어느 정도 이해하게 된다. 다만 기억을 그대로 코드에 반영해두지 않으면, 휘발성 기억이 되버린다.

 

리팩터링을 진행하면 코드가 깔끔하게 정리된다. 이렇게 정리되면 전에 보이면 설계가 눈에 들어온다.

 

리팩터링을 진행하면 코드를 보다 깊은 수준까지 이해하게 된다.

 

쓰레기 줍기 리팩터링

보이 스카우트 원칙에 기반한 내용

 

비효율적으로 처리하는 코드를 발견할 때가 있다.

 

로직이 쓸데없이 복잡하거나, 매개변수화한 함수 하나면 일을 거의 똑같은 함수 여러 개로 작성

 

이럴 간단하다면, 즉시 고치고, 시간이 걸린다면 메모를 해두고 하던 일을 마무리하고, 처리한다.

 

리팩터링의 멋진 점은 각각의 작은 단계가 코드를 깨뜨리지 않는다.

그래서 작업을 잘게 나눠 달에 걸쳐 진행하더라도 문제가 되지 않는다.

계획된 리팩터링과 수시로 하는 리팩터링

준비를 위한 리팩터링, 이해를 위한 리팩터링, 쓰레기 줍기 리팩터링은 기회가 때만 진행한다.

 

개발에 들어가기 전에 리팩터링 일정을 따로 잡지 않고, 기능 추가나 버그를 잡는 동안 함께 진행한다.

, 프로그래밍 과정에 녹여 자연스럽게 한다.

 

리팩터링은 프로그래밍과 구분되는 별개의 활동이 아니다.

마치 if 작성할 시간을 따로 잡지 않는 것과 같다.

 

계획된 리팩터링은 그동안 리팩터링에 소홀 했을 진행한다.

결과적으로 계획된 리팩터링을 안해도 되는 것이 좋다.(수시로 리팩터링 했다는 증거)

 

오래 걸리는 리팩터링

대부분의 리팩터링은 분에서 시간 사이다.

때론 전체가 붙어도 걸리는 대규모 리팩터링도 있다.

이런 상황에서도 전체가 리팩터링에 매달리는 것은 효율적이지 않다.

리팩터링은 중간에 멈춰도 코드를 깨트리지 않는 다는 장점이 있기 때문에 팀원 각자가 리팩터링할 코드를 만다면, 각자 조금씩 개선하는 식이 효율적이다.

 

코드 리뷰에 리팩터링 활용하기

경험이 많은 개발자의 노하우를 적은 개발자에게 전수할 있다.

눈엔 명확한 코드가 다른 팀원에게 명확하지 않을 있다.

코드 리뷰를 하면 다른 사람의 아이디어를 얻을 있다는 장점도 있다.

관리자에게는 뭐라고 말해야 할까?

상당수의 관리자와 고객은 코드베이스 상태가 생산성에 미치는 영향을 모른다.

이런 상황에 있는 이들에게는 "리팩터링한다고 말하지 말라" 조언한다.

 

개발자는 프로다. 프로 개발자는 소프트웨어를 최대한 빨리 만들어야 한다.

리팩터링은 결과적으로 소프트웨어 생산성을 높인다.

리팩터링하지 말아야

외부 API 다루듯 호출해서 쓰는 코드라면 지저분해도 그냥 둔다.

내부 동작을 이해해야 시점에 리팩터링해야 효과를 제대로 있기 때문이다.

 

리팩터링하는 보다 처음부터 새로 작성하는 쉬울 때도 리팩터링하지 않는다. 판단은 노련한 개발자가 아니면 내리기 힘들다.

'IT책, 강의 > 리팩터링' 카테고리의 다른 글

03 - 코드에서 나는 악취  (0) 2023.07.10
02 - 리팩터링 원칙 - 02  (0) 2023.07.08
01 - 리팩터링: 첫 번째 예시 - 02  (0) 2023.07.04
01 - 리팩터링: 첫 번째 예시 - 01  (0) 2023.07.02
00 - 들어가며  (0) 2023.07.01
 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

 

 

GitHub - rkwhr0010/refactoring: 리팩터링책 내용

리팩터링책 내용. Contribute to rkwhr0010/refactoring development by creating an account on GitHub.

github.com

1.6 계산 단계와 포맷팅 단계 분리하기

지금까진 프로그램의 논리적인 요소 파악을 위해 코드 구조를 보강했다.

리팩터링 초기 단계에서 흔히 수행한다.

 

골격은 충분히 개선 됐으니, 이제 원하는 기능 변경 작업을 차례다.

statement()함수의 HTML 버전 만들기

 

구조를 개선해서 처음 코드보다 작업하기 편해졌다.

계산코드가 모두 분리됐기 때문에 일곱 줄짜리 최상단 코드에 대응하는 HTML 버전만 작성하면 된다.

 

HTML버전 statement()함수를 만드는데 문제점이 있다. 계산 코드가 전부 내부 중첩 함수라 그것을 그대로 복사해서 만들면 코드 중복이 발생한다.

 

문제 해결법으로 가장 추천하는 방식은 단계 쪼개기다.

statement() 로직을 단계로 나누는 것이다.

단계는 statement() 필요한 데이터를 처리하고,

다음 단계는 앞서 처리한 결과를 텍스트나 HTML 표현하는

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))} (${perf.audience}석)\n`;
    }
    result += `총액: ${usd(totalAount())}\n`; 
    result += `적립 포인트: ${totalVolumeCredits()}점\n`;
    return result;
    function totalAount(){ 
        let result = 0;
        for(let perf of invoice.performances){
            result += amountFor(perf);
        }
        return result;
    }
…. 이하 추출한 본문




function statement(invoice, plays) {
    const statementData = {};
    return renderPlainText(statementData, invoice, plays) //중간데이터 구조를 인수로 전달
}
function renderPlainText(data, invoice, plays)

 

renderPlainText() 인수 invoice 중간 데이터구조로 옮기기

function statement(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances;
    return renderPlainText(statementData, /*invoice,*/ plays) //필요없어진 인수 삭제
}
function renderPlainText(data, plays) {
    let result = `청구 내역 (고객명 : ${data.customer})\n`;
    for(let perf of data.performances){
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
    }
    result += `총액: ${usd(totalAount())}\n`; 
    result += `적립 포인트: ${totalVolumeCredits()}점\n`;
    return result;
function totalAount(){

 

데이터 복사

function statement(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    return renderPlainText(statementData,  plays) 

    function enrichPerformance(aPerformance){
        const result = Object.assign({}, aPerformance);//얕은 복사 수행
        return result;
    }
}

이렇게 데이터를 복사해서 사용하는 이유는 데이터를 불변으로 취급하기 위함

function statement(invoice, plays) {
    const statementData = {};
    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){ //renderPlainText 중첩함수를 옮김
        return plays[aPerformance.playID];
    }
}

옮기면서 renderPlainText()에서 playFor()사용했던 부분 전부 변경

 

 

amountFor() 비슷하기 옮기기

function statement(invoice, plays) {
    const statementData = {};
    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);//중간 데이터에 연극 정보를 저장
        result.amount = amountFor(result);
        return result;
    }
    function playFor(aPerformance){ //renderPlainText 중첩함수를 옮김
        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 > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${aPerformance.play.type}`);
        }
        return result; 
    }
}

volumeCreditsFor()옮기기

function statement(invoice, plays) {
    const statementData = {};
    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);//중간 데이터에 연극 정보를 저장
        result.amount = amountFor(result);
        result.volumeCredits = volumeCreditsFor(result);
        return result;
    }
    function playFor(aPerformance){ //renderPlainText 중첩함수를 옮김
        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 > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${aPerformance.play.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 statement(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    statementData.totalAmount = totalAmount(statementData);
    statementData.totalVolumeCredits = totalVolumeCredits(statementData);
    return renderPlainText(statementData,  plays) 
    function enrichPerformance(aPerformance){
        const result = Object.assign({}, aPerformance);
        result.play = playFor(result);//중간 데이터에 연극 정보를 저장
        result.amount = amountFor(result);
        result.volumeCredits = volumeCreditsFor(result);
        return result;
    }
    function playFor(aPerformance){ //renderPlainText 중첩함수를 옮김
        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 > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${aPerformance.play.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.volumeCredits, 0 );
    }
    function totalVolumeCredits(data){
        return data.performances //반복문을 파이프라인으로 바꿈
            .reduce((total, p)=>total+p.volumeCredits, 0);
    }
}

statemnet() 필요한 데이터 처리 코드 함수로 빼기

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);
        statementData.totalVolumeCredits = totalVolumeCredits(statementData);
        return statementData; 
    }

중간 데이터 부분 함수로 빼기

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);
        statementData.totalVolumeCredits = totalVolumeCredits(statementData);
        return statementData; 
}

 

분리된 코드를 각 파일에 나눠서 별도 파일에 저장해 기존 코드를 재사용한다.

나는 실습 편의상 한 파일로 진행했다.

 

1.7 중간 점검: 파일( 단계) 분리됨

//statement.js 파일이라 가정
function statement(invoice, plays) {
    return renderPlainText(createStatementData(invoice, plays));
}
function htmlStatement(invoice, plays){
    return renderHtml(createStatementData(invoice,plays));
    //중간데이터 생성 함수를 공유한다.
}
function renderHtml(data, plays) {
    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><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`;
    return result;
}
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 usd(aNumber){
    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        minimumFractionDigits: 2
    }).format(aNumber/100); 
}
//statement.js 파일이라 가정 끝

//createStatementData.js 별도 파일이라 가정
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 = playFor(result);
        result.amount = amountFor(result);
        result.volumeCredits = volumeCreditsFor(result);
        return result;
    }
    function playFor(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 > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${aPerformance.play.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);
    }
}
//createStatementData.js 별도 파일이라 가정 끝

전체 로직을 구서하는 요소들이 뚜렸하게 분리되면서 파악이 쉬워졌다.

그리고 모듈화하면서 html버전도 재사용으로 쉽게 만들었다.

프로그래밍에선 명료함이 중요하다.

 

 

 

1.8 다형성을 활용해 계산 코드 재구성하기

amountFor()함수를 보면 연극 장르에 따라 계산 방식이 달라진다.

이런 조건부 로직은 코드 수정 획수가 늘어날수록 골칫거기로 전락한다.

 

"조건부 로직을 다형성으로 바꾸기"

리팩터링은 조건부 코드 덩어리를 다형성을 활용하는 방식으로 바꿔준다.

리팩터링을 적용하려면 상속 계층부터 정의해야 한다.

 

공연료 계산기 만들기

여기서 핵심은 각 공연 정보를 중간 데이터 구조에 채워주는 enrichPerformance()함수

이 함수는 조건부 로직을 포함한 함수인 amountFor()와 volumeCreditsFor()를 호출한다.

두 함수를 전용 클래스로 옮기는 작업을 수행한다.

 

createStatementData()함수속
...
    function enrichPerformance(aPerformance){
        const calculator = new PerformanceCalculator(aPerformance); //공연료 계산기 생성
        const result = Object.assign({}, aPerformance);
        result.play = playFor(result);
        result.amount = amountFor(result);
        result.volumeCredits = volumeCreditsFor(result);
        return result;
    }


최상위
class PerformanceCalculator{
    constructor(aPerformance){
        this.performance = aPerformance;
    }
}

함수들을 계산기로 옮기기

//createStatementData.js 별도 파일이라 가정

class PerformanceCalculator{
    constructor(aPerformance, aPlay){
        this.performance = aPerformance;
        this.play = aPlay;
    }
    get amount(){
        let result = 0;
        switch(this.play.type){
            case "tragedy" : 
            result = 40000;
            if(this.performance.audience > 30){
                result += 1000 * (this.performance.audience - 30);
            }
            break;
            case "comedy" : 
            result = 30000;
            if(this.performance.audience > 30){
                result += 10000 + 500 * (this.performance.audience - 20);
            }
            result += 300 * this.performance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${this.play.type}`);
        }
        return result; 
    }
    get volumeCredits(){
        let result = 0;
        result += Math.max(this.performance.audience -30,0);
        if("comedy" === this.play.type) 
        result += Math.floor(this.performance.audience/5);
        return result;
    }
}
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 calculator = new PerformanceCalculator(aPerformance, playFor(aPerformance)); //공연 정보를 계산기로 전달
        const result = Object.assign({}, aPerformance);
        result.play = calculator.play;
        result.amount = calculator.amount;
        result.volumeCredits = calculator.volumeCredits;
        return result;
    }
    function playFor(aPerformance){ 
        return plays[aPerformance.playID];
    }

    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);
    }
}
//createStatementData.js 별도 파일이라 가정 끝

 

 

공연료 계산기를 다형성 버전으로 만들기

클래스에 로직을 담았으니 다형성을 지원하도록 만든다.

먼저 타입 코드를 서브클래스로 바꾸기를 진행한다.

 

그러기 위해선 클래스를 직접적으로 생성하는 코드를 팩토리 함수를 호출하게 하여 의존성을 낮춰야 한다.


class PerformanceCalculator{
    constructor(aPerformance, aPlay){
        this.performance = aPerformance;
        this.play = aPlay;
    }
    get amount(){
        return new Error('서브클래스에서 처리하도록 설계되었습니다.');
    }
    get volumeCredits(){
        let result = 0;
        result += Math.max(this.performance.audience -30,0);
        if("comedy" === this.play.type) 
        result += Math.floor(this.performance.audience/5);
        return result;
    }
}
class TragedyCalcultor extends PerformanceCalculator{
    get amount(){
        let result = 0;
        result = 40000;
        if(this.performance.audience > 30){
            result += 1000 * (this.performance.audience - 30);
        }
        return result;
    }
}
class ComedyCalcultor extends PerformanceCalculator{
    get amount(){
        let result = 0;
        result = 30000;
        if(this.performance.audience > 30){
            result += 10000 + 500 * (this.performance.audience - 20);
        }
        result += 300 * this.performance.audience;
        return result;
    }
}
function createPerformanceCalculator(aPerformance, aPlay){
    switch(aPlay.type){
        case "tragedy" : return new TragedyCalcultor(aPerformance,aPlay);
        case "comedy" : return new ComedyCalcultor(aPerformance,aPlay);
        default: throw new Error(`알 수  없는 장르 : ${aPlay.type}`);
    }
}

 

1.9 상태 점검 : 다형성을 활용하여 데이터 생성하기

//createStatementData.js 별도 파일이라 가정

class PerformanceCalculator{
    constructor(aPerformance, aPlay){
        this.performance = aPerformance;
        this.play = aPlay;
    }
    get amount(){
        return new Error('서브클래스에서 처리하도록 설계되었습니다.');
    }
    get volumeCredits(){
        return Math.max(this.performance.audience -30,0);
    }
}
class TragedyCalcultor extends PerformanceCalculator{
    get amount(){
        let result = 0;
        result = 40000;
        if(this.performance.audience > 30){
            result += 1000 * (this.performance.audience - 30);
        }
        return result;
    }
}
class ComedyCalcultor extends PerformanceCalculator{
    get amount(){
        let result = 0;
        result = 30000;
        if(this.performance.audience > 30){
            result += 10000 + 500 * (this.performance.audience - 20);
        }
        result += 300 * this.performance.audience;
        return result;
    }
    get volumeCredits(){
        return super.volumeCredits + Math.floor(this.performance.audience/5);
    }
}
function createPerformanceCalculator(aPerformance, aPlay){
    switch(aPlay.type){
        case "tragedy" : return new TragedyCalcultor(aPerformance,aPlay);
        case "comedy" : return new ComedyCalcultor(aPerformance,aPlay);
        default: throw new Error(`알 수  없는 장르 : ${aPlay.type}`);
    }
}
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 calculator = createPerformanceCalculator(aPerformance, 
            playFor(aPerformance)); //생성자 대신 팩토리 함수 이용
        const result = Object.assign({}, aPerformance);
        result.play = calculator.play;
        result.amount = calculator.amount;
        result.volumeCredits = calculator.volumeCredits;
        return result;
    }
    function playFor(aPerformance){ 
        return plays[aPerformance.playID];
    }

    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);
    }
}
//createStatementData.js 별도 파일이라 가정 끝

조건부 로직을 생성 함수하나로 옮겼다.

연극 장르별 계산 코드들을 함께 묶었다.

 

1.10 마치며

리팩터링을 크게 3 단계로 진행했다.

1단계 : 원본 함수를 중첩 함수 여러개로 나누기

2단계 : 쪼개기를 적용, 계산 코드/출력 코드 분리

3단계 : 계산 로직을 다형성으로 표현

 

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

코드를 수정할 때 고쳐야 할 곳을 쉽게 찾고, 오류 없이 빠르게 수정할 수 있어야 한다.

고객의 요구사항을 빠르게 반영할 수 있어야 한다.

 

 

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
<script src="plays.json" type="text/javascript"></script>
<script src="invoices.json" type="text/javascript"></script>
<script>



//statement.js 파일이라 가정
function statement(invoice, plays) {
    return renderPlainText(createStatementData(invoice, plays));
}
function htmlStatement(invoice, plays){
    return renderHtml(createStatementData(invoice,plays));
    //중간데이터 생성 함수를 공유한다.
}
function renderHtml(data, plays) {
    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><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`;
    return result;
}

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 usd(aNumber){
    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        minimumFractionDigits: 2
    }).format(aNumber/100); 
}
//statement.js 파일이라 가정 끝


//createStatementData.js 별도 파일이라 가정


class PerformanceCalculator{
    constructor(aPerformance, aPlay){
        this.performance = aPerformance;
        this.play = aPlay;
    }
    get amount(){
        return new Error('서브클래스에서 처리하도록 설계되었습니다.');
    }
    get volumeCredits(){
        return Math.max(this.performance.audience -30,0);
    }
}
class TragedyCalcultor extends PerformanceCalculator{
    get amount(){
        let result = 0;
        result = 40000;
        if(this.performance.audience > 30){
            result += 1000 * (this.performance.audience - 30);
        }
        return result;
    }
}
class ComedyCalcultor extends PerformanceCalculator{
    get amount(){
        let result = 0;
        result = 30000;
        if(this.performance.audience > 30){
            result += 10000 + 500 * (this.performance.audience - 20);
        }
        result += 300 * this.performance.audience;
        return result;
    }
    get volumeCredits(){
        return super.volumeCredits + Math.floor(this.performance.audience/5);
    }
}

function createPerformanceCalculator(aPerformance, aPlay){
    switch(aPlay.type){
        case "tragedy" : return new TragedyCalcultor(aPerformance,aPlay);
        case "comedy" : return new ComedyCalcultor(aPerformance,aPlay);
        default: throw new Error(`알 수  없는 장르 : ${aPlay.type}`);
    }
}

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 calculator = createPerformanceCalculator(aPerformance, 
            playFor(aPerformance)); //생성자 대신 팩토리 함수 이용
        const result = Object.assign({}, aPerformance);
        result.play = calculator.play;
        result.amount = calculator.amount;
        result.volumeCredits = calculator.volumeCredits;

        return result;
    }
    function playFor(aPerformance){ 
        return plays[aPerformance.playID];
    }
    
    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);
    }
}

//createStatementData.js 별도 파일이라 가정 끝

console.log(statement(invoices,plays));
document.write(htmlStatement(invoices,plays));
</script>
</body>
</html>

 

 

 

 

'IT책, 강의 > 리팩터링' 카테고리의 다른 글

03 - 코드에서 나는 악취  (0) 2023.07.10
02 - 리팩터링 원칙 - 02  (0) 2023.07.08
02 - 리팩터링 원칙 - 01  (0) 2023.07.06
01 - 리팩터링: 첫 번째 예시 - 01  (0) 2023.07.02
00 - 들어가며  (0) 2023.07.01
 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

 

이 책은 개발자를 대상으로 쓰여진 책이기 때문에 흥미를 위해 첫 장부터 리팩터링을 수행하는 방식으로 진행한다.

첫 장부터 리팩터링이 나오니 그 과정을 전부 파악할 순 없다. 그냥 진행되는 과정에 감을 잡는 정도면 충분하다.

 

예제

https://github.com/rkwhr0010/refactoring/tree/main/refactoring/chap01

 

GitHub - rkwhr0010/refactoring: 리팩터링책 내용

리팩터링책 내용. Contribute to rkwhr0010/refactoring development by creating an account on GitHub.

github.com

리팩터링 과정마다 기존 코드를 커밋&푸시해 갱신하는 방식이 아닌 Ex01 ==> Ex02 이렇게 갱신하는 방식으로 따라했다.

 

폴더 구조는 해당 목차에 리팩터링이 없다면, 건너뛴다.

1.1 자, 시작해보자!

	
	plays = {
	    "hamlet":{"name" : "Hamlet", "type" : "tragedy"},
	    "as-like" : {"name":"As You Like It", "type" : "comedy"},
	    "othello":{"name":"Othello", "type":"tragedy"}
	}
	
	invoices = 
	    {
	        "customer" : "BigCo" ,
	        "performances" : [
	            {
	                "playID":"hamlet",
	                "audience" : 55
	            },
	            {
	                "playID" :"as-like",
	                "audience": 35
	            },
	            {
	                "playID":"othello",
	                "audience" :40
	                
	            }
	        ]
	    }
	
	
	<!DOCTYPE html>
	<html lang="en">
	<head>
	    <meta charset="UTF-8">
	    <meta http-equiv="X-UA-Compatible" content="IE=edge">
	    <meta name="viewport" content="width=device-width, initial-scale=1.0">
	    <title>Document</title>
	</head>
	<body>
	    <script src="plays.json" type="text/javascript"></script>
	    <script src="invoices.json" type="text/javascript"></script>
	<script>
	    //기본 함수
	    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 += 1000 * (perf.audience - 30);
	                }
	                break;
	                case "comedy" : //희극
	                thisAmount = 30000;
	                if(perf.audience > 30){
	                    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;
	    }
	    console.log(statement(invoices,plays))
	</script>
	</body>
</html>

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

공연료 관련 프로그램, 예제 코드 출력 시 위와 같은 결과 출력

 

1.2 예시 프로그램을 본소감

프로그램이 작동하는 상황에서 그저 코드가 "지저분하다" 이유로 불평하는 것은 프로그램 구조를 너무 미적인 기준으로만 판단하는 걸까?

컴파일러는 코드가 이쁘건 말건 상관없다. 하지만 그 코드를 수정하는 사람은 미적인 상태에 민감하다

설계가 나쁘면 수정이 어렵다. 수정할 부분을 특정하기도 힘들고 그과정에서 실수를 하기 쉽상이다.

 

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

HTML 출력하는 기능을 추가해야한다고 가정하자

HTML 태그를 전부 넣어야되니 함수 복잡도가 증가하기에 기존 함수를 복사해서  HTML버전을 만드는 것이 일반적일 것이다. 이러면 중복코드도 발생할 것이고, statement()함수 수정이 생기면 HTML버전도 함께 수정해야 한다.

 

리팩터링이 필요한 이유는 이러한 변경 때문이다.

리팩터링은 지금 코드가 절대 변경될 없고, 동작한다면 굳이 필요 없다.

 

 

1.3 리팩터링의 첫 단계

리팩터링 단계는 항상 같다. 리팩터링할 코드 영역을 꼼꼼하게 검사해줄 테스트 코드부터 마련한다.

이유는 리팩터링 기법들의 버그 발생 여지를 최소화하기 위함이다.

리팩터링 또한 코드를 변경하는 행위로 버그가 충분히 스며들 가능성이 있다,

변경 때마다 테스트 코드를 돌린다

테스트 코드는 작성하는데 시간이 걸리지만, 나중에 디버깅 시간이 줄어 결과적으로 전체 작업 시간이 감소된다.

 

 

1.4 statement()함수 쪼개기

함수를 리팩터링할 때는 먼저 전체 동작을 각각의 부분으로 나눌 있는 지점을 찾는다.

 

switch(play.type){
    case "tragedy" : //비극
    thisAmount = 40000;
    if(perf.audience > 30){
        thisAmount += 1000 * (perf.audience - 30);
    }
    break;
    case "comedy" : //희극
    thisAmount = 30000;
    if(perf.audience > 30){
        thisAmount += 10000 + 500 * (perf.audience - 20);
    }
    thisAmount += 300 * perf.audience;
    break;
    default: throw new Error(`알수 없는 장르: ${play.type}`);
}

switch문을 보면, 공연에 대한 요금을 계산하고 있다.

이러한 사실은 코드를 분석해서 얻은 정보다. 이런 식으로 파악한 정보는 휘발성이 강해 다음에 코드를 다시 본다면 까먹는 경우가 많다.

 

코드를 별도 함수로 추출해 명확한 이름을 붙이자. amountFor()

추가로 함수 내부에서 쓰이는 변수, 파라미터 이름도 명확하게 변경한다.

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

좋은 코드라면 의도가 명확히 표현돼야 한다. 요즘 IDE 이름변경이 매우 쉬우므로 좋은 이름으로 바꾸길 주저하지 말자

//기본 함수
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;

    function amountFor(aPerformance, play) {//명확한 이름으로 변경
        let result = 0;

        switch(play.type){
            case "tragedy" : //비극
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : //희극
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${play.type}`);
        }
        return result; 
    }
}

 

play 변수 제거하기

aPerformance 루프 변수에서 오기에 순회마다 값이 변경된다.

반면에 play 개별공연(aPerformance)에서 얻기 때문에 매개변수로 전달할 필요가 없다.

amountFor() 안에서 다시 계산하면 된다.

이런 식으로 함수를 쪼갤 때마다 불필요한 매개변수를 최대한 제거한다.

이러한 임시 변수들 때문에 로컬 범위에 존재하는 이름이 많아져 추출 작업이 복잡해진다.

 for(let perf of invoice.performances){
        const play = playFor(perf); // 우변을 함수로 추출
... 

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

정상동작 테스트함 다음 과정 변수 인라인하기

 

for(let perf of invoice.performances){
    // const play = playFor(perf); //변수 인라인하기
    let thisAmount = amountFor(perf, playFor(perf));
    //포인트를 적립한다.
    volumeCredits += Math.max(perf.audience -30,0);
    //희극 관객 5명 마다 추가 포인트를 제공한다.
    if("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience/5);
    //청구 내역을 출력한다.
    result += `${playFor(perf).name} : ${format(thisAmount/100)} (${perf.audience}석)\n`;
    totalAmount += thisAmount;
}

amountFor()함수에서 playFor()함수로 인해 매개변수를 하나 줄일 있게 됐다.

    function amountFor(aPerformance/*, play*/) {//필요없어진 매개변수제거
        let result = 0;
    
        switch(playFor(aPerformance).type){
            case "tragedy" : //비극
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : //희극
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${playFor(aPerformance).type}`);
        }
        return result; 
    }

지역 변수를 제거해서 함수 추출하기가 훨씬 쉬워졌다.

이유는 유효범위를 신경써야할 대상이 줄었기 때문이다.

추출 작업 전에 거의 항상 지역 변수부터 제거하도록 하자

 

amountFor()함수 파라미터는 정리했으니 statement() 보자

임시 변수인 thisAmount 선언되고 값이 변하지 않는다.

따라서 "변수 인라인하기" 적용한다.

 

	statement()속…
	
	    for(let perf of invoice.performances){
        	//let thisAmount = amountFor(perf); //thisAmount변수 인라인
	        //포인트를 적립한다.
	        volumeCredits += Math.max(perf.audience -30,0);
	        //희극 관객 5명 마다 추가 포인트를 제공한다.
	        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);//thisAmount변수 인라인
	    }

 

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

play 변수를 제거한 결과 로컬 유효범위의 변수가 하나 줄어서 적립 포인트 계산 부분을 추출하기가 쉬워졌다.

 

	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){
	        //포인트를 적립한다.
	        volumeCredits += Math.max(perf.audience -30,0);
	        //희극 관객 5명 마다 추가 포인트를 제공한다.
	        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);//thisAmount변수 인라인
	    }
	    result += `총액: ${format(totalAmount/100)}\n`;
	    result += `적립 포인트: ${volumeCredits}점\n`;
	    
	    return result;
}

처리해야 변수 perf, volumeCredits

volumeCredits처리
//함수 추출
function 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;
}

 ...............

    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 volumeCredits = 0;
    volumeCredits += Math.max(aPerformance.audience -30,0);
    if("comedy" === playFor(aPerformance).type) 
        volumeCredits += Math.floor(aPerformance.audience/5);
    return volumeCredits;
}

 

format 변수 제거하기

임시 변수는 나중에 문제를 일으킬 있다.

임시 변수는 자신이 속한 루틴에서만 의미가 있어서 루틴이 길고 복잡해지기 쉽다.

 

임시 변수를 제거하는 것이 다음 리팩터링

 

리팩토링 대상코드

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

format 임시 변수에 함수를 대입한 형태다.(함수 포인터처럼)

직접 함수를 선언해 사용하도록 바꾼다.

function statement(invoice, plays) {
    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
    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`;//임시 변수였던 format을 함수 호출로 대체
    result += `적립 포인트: ${volumeCredits}점\n`;

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

format만으로 함수가 하는 일을 충분히 설명하지 못한다. 함수에 핵심 기능에 걸맞는 이름을 짓는다.(화폐 단위 맞추기)

    for(let perf of invoice.performances){
        volumeCredits += volumeCreditsFor(perf);
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    result += `총액: ${usd(totalAmount)}\n`;
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;
}
function usd(aNumber){
    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        minimumFractionDigits: 2
    }).format(aNumber/100); // 단위 변환 로직도 이 함수로 안으로 이동
}

함수를 작게 쪼개는 리팩터링은 이름을 잘지어야만 효과가 있다.

이름이 좋으면 함수 본문을 읽지 않고도 무슨 일을 하는지 있다.

 

이름 바꾸기는 쉬우므로 이름 짓기를 주저하지 말고 짓자. 좋은 이름이 떠올르면 다시 변경하면 그만이다.

 

 

volumeCredits 변수 제거하기

변수는 반복문을 바퀴 때마다 값을 누적하기 때문에 리팩터링하기가 까다롭다.

function statement(invoice, plays) {
    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
    for(let perf of invoice.performances){
        volumeCredits += volumeCreditsFor(perf);
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    result += `총액: ${usd(totalAmount)}\n`;
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;
}

먼저 반복문 쪼개기로 volumeCredits 값이 누적되는 부분을 따로 빼낸다.

위를 아래처럼

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

    for(let perf of invoice.performances){
        //청구 내역을 출력한다.
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    let volumeCredits = 0;// 변수 선언을 반복문 앞으로 옮김
    for(let perf of invoice.performances){ // 값 누적 로직을 별도  for문으로 분리
        volumeCredits += volumeCreditsFor(perf);
    }
    result += `총액: ${usd(totalAmount)}\n`;
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;
}

이렇게 분리하면 임시 변수를 질의 함수로 바꾸기가 수월해진다.

함수로 추출한 것으로 변수 인라인을 한다.

    for(let perf of invoice.performances){
        //청구 내역을 출력한다.
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    result += `총액: ${usd(totalAmount)}\n`;
    result += `적립 포인트: ${totalVolumeCredits()}점\n`;
return result;

반복문이 중복되는 것을 꺼리는 이들이 많다.

이정도 중복은 성능에 미치는 영향이 미미할 때가 많다.

 

실제로 리펙터링한 결과를 비교해도 실행 시간 차이가 거의 없다.

 

경험 많은 프로그래머조차 코드의 실제 성능을 정확히 예측하지 못한다.

이유는 똑똑한 컴파일러들은 최신 캐싱 기법 등으로 무장하고 있어서 우리의 직관을 초월하는 결과를 내어주기 때문이다.

또한 소프트웨어 성능은 대체로 코드의 몇몇 작은 부분에 의해 결정되므로 외의 부분은 수정한다고 해도 성능 차이를 체감할 없다.

 

때로는 리팩터링이 성능에 상당한 영향을 주기도 한다. 그런 경우라도 개의치 않고 리팩터링한다.

다음어진 코드라야 성능 개선 작업도 훨씬 수월하기 때문이다.

추가로 시간내어 작업을 한다면 결과적으로 깔끔하고, 빠른 코드를 얻게 된다.

 

따라서 리팩터링으로 인한 성능 문제에 대한 저자의 조언은

"특별한 경우가 아니라면 일단 무시하라"

리팩터링 성능이 떨어진다면, 그때 성능 개선하자

 

리팩터링 중간에 테스트가 실패하고 원인을 바로 찾기 못하면 가장 최근 커밋으로 돌아가서 리팩터링의 단계를 작게 나눠서 다시 시도할 있다.

 

코드가 복잡할 수록 단계를 작게 나누면 작업 속도가 빨라지기 때문이다.

 

totalAmount 앞에 과정과 동일하게 제거한다.

반복문 쪼개고, 변수 초기화 문장을 옮기고, 함수를 추출,

	    for(let perf of invoice.performances){
	        //청구 내역을 출력한다.
	        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
	    }
	    let totalAmount = tmpFunction();
	    function tmpFunction(){ //함수 추출/ 이름은 임시로
	        let totalAmount = 0;
	        for(let perf of invoice.performances){
	            totalAmount += amountFor(perf);
	        }
	        return totalAmount;
	    }
	
	…..
	
	
	    function totalAount(){ 
	        let result = 0; // 함수 안 변수이름도 자기 스타일에 맞게 변경
	        for(let perf of invoice.performances){
	            result += amountFor(perf);
	        }
	        return result;
	    }
	    result += `총액: ${usd(totalAount())}\n`; //함수 인라인 후 의미있는 이름으로 변경하기
	    result += `적립 포인트: ${totalVolumeCredits()}점\n`;

 

1.5 중간 점검: 난무하는 중첩 함수

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
<script src="plays.json" type="text/javascript"></script>
<script src="invoices.json" type="text/javascript"></script>
<script>

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

    //중첩 함수 시작
    function totalAount(){ 
        let result = 0;
        for(let perf of invoice.performances){
            result += amountFor(perf);
        }
        return result;
    }

    function totalVolumeCredits(){
        let volumeCredits = 0;
        for(let perf of invoice.performances){ 
            volumeCredits += volumeCreditsFor(perf);
        }
        return volumeCredits;
    }
    function usd(aNumber){
        return new Intl.NumberFormat("en-US", {
            style: "currency",
            currency: "USD",
            minimumFractionDigits: 2
        }).format(aNumber/100); 
    }
    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 playFor(aPerformance){
        return plays[aPerformance.playID];
    }
    function amountFor(aPerformance) {
        let result = 0;
        switch(playFor(aPerformance).type){
            case "tragedy" : 
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : 
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${playFor(aPerformance).type}`);
        }
        return result; 
    }
    
}

console.log(statement(invoices,plays));
</script>

</body>

</html>

최상위 statement()함수는 이제 일곱줄 뿐이며, 출력할 문장 생성 역할만 한다.

 

계산 로직은 여러 보조 함수로 빼냈다.

전체적인 흐름을 이해하기가 훨씬 쉬워졌다.

 

 

 

 

 

 

 
 

<!DOCTYPE html>
<html lang="ko">

<헤드>
<메타 문자셋="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>문서</title>
</헤드>

<몸>
<script src="plays.json" type="text/javascript"></script>
<script src="invoices.json" type="text/javascript"></script>
<스크립트>

기능문(인보이스, 재생) {
let result = ` 청구 내역 (고객명 : ${invoice.customer})\n`;
for(invoice.performances의 let perf){
result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
}
result += `전체: ${usd(totalAount())}\n`;
result += `적립 포인트: ${totalVolumeCredits()}점\n`;
반환 결과;

//중첩 맛있게 시작
함수 totalAount(){
let 결과 = 0;
for(invoice.performances의 let perf){
결과 += amountFor(perf);
}
반환 결과;
}

함수 totalVolumeCredits(){
let volumeCredits = 0;
for(invoice.performances의 let perf){
volumeCredits += volumeCreditsFor(perf);
}
반환 volumeCredits;
}
함수 usd(숫자){
새로운 Intl.NumberFormat("en-US", {를 반환합니다.
스타일: "통화",
통화: "USD",
minimumFractionDigits: 2
}).format(숫자/100);
}
기능 volumeCreditsFor(aPerformance){
let volumeCredits = 0;
volumeCredits += Math.max(aPerformance.audience -30,0);
if("코미디" === playFor(aPerformance).type)
volumeCredits += Math.floor(aPerformance.audience/5);
반환 volumeCredits;
}
함수 playFor(aPerformance){
반환 재생[aPerformance.playID];
}
함수 amountFor(aPerformance) {
let 결과 = 0;
switch(playFor(aPerformance).type){
사례 "비극":
결과 = 40000;
if(aPerformance.audience > 30){
결과 += 1000 * (aPerformance.audience - 30);
}
부서지다;
케이스 "코미디":
결과 = 30000;
if(aPerformance.audience > 30){
결과 += 10000 + 500 * (aPerformance.audience - 20);
}
결과 += 300 * aPerformance.audience;
부서지다;
default: throw new Error(`알수없는 장르: ${playFor(aPerformance).type}`);
}
반환 결과;
}

}

console.log(statement(invoices,plays));
</스크립트>

</body>

</html>

 

'IT책, 강의 > 리팩터링' 카테고리의 다른 글

03 - 코드에서 나는 악취  (0) 2023.07.10
02 - 리팩터링 원칙 - 02  (0) 2023.07.08
02 - 리팩터링 원칙 - 01  (0) 2023.07.06
01 - 리팩터링: 첫 번째 예시 - 02  (0) 2023.07.04
00 - 들어가며  (0) 2023.07.01

+ Recent posts