리팩터링 | 마틴 파울러 | 한빛미디어- 교보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 끝나지 않은 여정

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

 

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

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

 

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

 

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

 

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

 

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

 

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

 

 

 

+ Recent posts