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

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

ebook-product.kyobobook.co.kr

개요

목적을 달성하는 방법은 여러 가지

기존 방식보다 쉬운 방법을 발견하면 변경한다.

 

예를들어 기존 기능을 똑같이 지원하는 라이브러리를 찾았다면 라이브러리를 쓰는 편이 좋을

 

작업을 위해선 메서드 기능을 가능한 잘게 나눠야 한다.

 

예시



function foundPerson(people){
    for(let i = 0; i < people.length; i++){
        if(people[i] === "Don"){
            return "Don";
        }
        if(people[i] === "John"){
            return "John";
        }
        if(people[i] === "Kent"){
            return "Kent";
        }
    }
    return "";
}


function foundPerson(people){
    const candidates = ["Don", "John", "Kent"];
    return people.find(p=> candidates.includes(p))||'';
}

 

 

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

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

ebook-product.kyobobook.co.kr

 

개요

클라이언트가 서버의 위임 객체의 다른 기능을 사용하고 싶을 마다 위임 메서드를 추가해야한다. 과도하면 단순히 위임 메서드만 들어나 성가셔진다.

 

서버는 단순히 중개자 역할로 전락할 있어 이럴 바엔 그냥 클라이언트가 위임 객체를 직접 호출하는 나을 있다.

데메테르 법칙을 너무 지나치게 지킬 이런 상황이 발생한다.

 

최소 지식 원칙 == 데메테르 법칙

내부 정보를 가능한 숨기고 밀접한 모듈과만 상호작용하여 결합도를 낮추자는 원칙

과정에서 래퍼 클래스나 위임 객체가 너무 늘어나는 부작용이 있다.

 

균형을 잡는 것이 중요하다.

균형점을 잡는 것은 너무 고민하지 않아도 된다. 리팩터링으로 다시 변경하기 쉽기 때문이다.

 

예시

Department 객체를 통해 manager 찾는다.

 



//클라이언트
let manager = aPerson.manager;
class Person{
    get manager(){return this._department.manager;}
}
class Department{
    get manager(){return this._manager;}
}


//클라이언트
let manager = aPerson.department.manager;
class Person{
    //게터 추가 후 클라이언트 코드 수정 후 위임(중개) 메서드 제거
    get department(){return this._department;}
}
class Department{
    get manager(){return this._manager;}
}

 

중개 메서드는 균형이 중요하다. 반드시 하나를 정해서 적용하거나 적용하지 않거나 필요가 없다.

자신이 처한 상황에 따라 절적히 섞어쓰는 지혜가 필요하다.

 

 

 

개요

모듈화 설계를 제대로하는 핵심은 캡슐화

캡슐화는 모듈들이 시스템의 다른 부분에 대해 알아야 내용을 줄여 준다.

코드 변경이 쉬워진다.

 

서버 객체의 필드가 가리키는 객체(위임) 메서드를 호출하려면 클라이언트는 위임 객체를 알아야 한다.

위임 객체 인터페이스 변경 인터페이스를 사용하는 모든 클라이언트 코드를 수정해야 한다.

이런 의존성을 없애기 위해 서버 클래스에서 자체 위임 메서드를 만들어 위임 객체의 존재를 숨기면 된다.

 

 

예시

class Person{
    constructor(name){
        this._name = name;
    }
    get name(){return this._name;}
    get department(){return this._department;}
    set department(arg){this._department = arg;}
}
class Department{
    get chargeCode(){return this._chargeCode;}
    set chargeCode(arg){this._chargeCode=arg;}
    get manager(){return this._manager;}
    set manager(arg){this._manager=arg;}
}
//클라이언트
let manager = aPerson.department.manager;


class Person{
    constructor(name){
        this._name = name;
    }
    get name(){return this._name;}
    get department(){return this._department;}
    set department(arg){this._department = arg;}
    get manager(){return this.department.manager;}
}
class Department{
    get chargeCode(){return this._chargeCode;}
    set chargeCode(arg){this._chargeCode=arg;}
    get manager(){return this._manager;}
    set manager(arg){this._manager=arg;}
}
//클라이언트
let manager = aPerson.manager;


class Person{
    constructor(name){
        this._name = name;
    }
//접근자 삭제
    get name(){return this._name;}
    get manager(){return this.department.manager;}
}
class Department{
    get chargeCode(){return this._chargeCode;}
    set chargeCode(arg){this._chargeCode=arg;}
    get manager(){return this._manager;}
    set manager(arg){this._manager=arg;}
}
//클라이언트
let manager = aPerson.manager;

 

 

 

 

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

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

ebook-product.kyobobook.co.kr

 

개요

클래스 추출하기의 정반대 리팩터링

이상 제역할을 못하는 클래스를 인라인한다. 판단 기준은 이상 클래스 역할이 거의 없을 때이다. 옮기는 위치는 클래스를 자주 사용하는

 

다른 케이스로 클래스의 기능을 지금과 다르게 배분하고 싶을 때도 일단 클래스를 인라인한다. 그리고 다시 클래스를 추출한다.

상황에 따라 컨텍스트 요소를 하나씩 옮기는 편할 수도, 아니면 일단 클래스를 인라인하고, 다시 클래스 추출하기가 편할 수도 있다.

 

예시

//현재 제일을 못하는 클래스라 가정
class TrackingInformation{
    get shippingCompany(){return this._shippingCompany;}
    set shippingCompany(arg){this._shippingCompany = arg;}
    get trackingNumber(){return this._trackingNumber;}
    set trackingNumber(arg){this._trackingNumber = arg;}
    get display(){
        return `${this.shippingCompany} : ${this.trackingNumber}`;
    }
}
class Shipment{
    get trackingInfo(){
        return this._trackingInformation.display;
    }
    get trackingInformation() {return this._trackingInformation;}
    set trackingInformation(aTrackingInformation) {
        this._trackingInformation = aTrackingInformation;
    }
}
//클라이언트
aShipment.trackingInformation.shippingCompany = request.vender;

class Shipment{
    get trackingInfo(){
        return this._trackingInformation.display;
    }
    get shippingCompany(){return this._trackingInformation.shippingCompany;}
    set shippingCompany(arg){this._trackingInformation.shippingCompany = arg;}
    get trackingInformation() {return this._trackingInformation;}
    set trackingInformation(aTrackingInformation) {
        this._trackingInformation = aTrackingInformation;
    }
}
//클라이언트- 호출부분에서 TrackingInformation 제거
let aShipment = new Shipment();
aShipment.shippingCompany = request.vender;


class Shipment{
    get trackingInfo(){
        return this._trackingInformation.display;
    }
    get display(){
        return `${this.shippingCompany} : ${this.trackingNumber}`;
    }
    //나머지도 옮긴 후 클래스 TrackingInformation 제거
    get shippingCompany(){return this._shippingCompany;}
    set shippingCompany(arg){this._shippingCompany = arg;}
    get display(){
        return `${this.shippingCompany} : ${this.trackingNumber}`;
    }
    get trackingInformation() {return this._trackingInformation;}
    set trackingInformation(aTrackingInformation) {
        this._trackingInformation = aTrackingInformation;
    }
}
let aShipment = new Shipment();
aShipment.shippingCompany = request.vender;

 

 

개요

함수 안에서 임시 변수는 값을 함수 안에서 다시 사용하거나 값에 명확한 이름을 부여하기 위한 수단이다.

 

함수를 별도 함수로 추출할 변수들이 함수로 만들어져 있으면 수월하다. 추출할 함수에 변수를 따로 전달할 필요가 없기 때문이다.

추가로 추출한 함수와 원래 함수의 경계가 분명해진다. 부자연스러운 의존 관계가 제거되기 때문이다.

 

변수 대신 함수로 만들어 두면, 혹시나 다른 곳에서 같은 방식으로 계산하는 변수를 찾으면 재사용할 기회를 얻는다.

 

이런 변수는 특히 클래스 리팩터링에 효과적이다. 같은 문맥을 공유하기 때문이다.

 

리팩터링은 계산된 뒤에 반드시 읽기만 해야한다.

예시



class Order {
    constructor(quantity, item){
        this._quantity = quantity;
        this._item = item;
    }
    get price(){
        var basePrice = this._quantity * this._item.price;
        var discountFactor = 0.98;
        if(basePrice>1000) discountFactor -= 0.03;
        return basePrice * discountFactor;
    }
}


    get price(){
        //var const로 바꿔보기 봇보고 지나진 대입문을 찾을 수 있다
        const basePrice = this._quantity * this._item.price;
        var discountFactor = 0.98;
        if(basePrice>1000) discountFactor -= 0.03;
        return basePrice * discountFactor;
    }


    get price(){
        const basePrice = this.basePrice();
        var discountFactor = 0.98;
        if(basePrice>1000) discountFactor -= 0.03;
        return basePrice * discountFactor;
    }
    //게터로 추출
    get basePrice() {
        return this._quantity * this._item.price;
    }


    get price(){
        var discountFactor = 0.98;
        //변수 인라인
        if(this.basePrice>1000) discountFactor -= 0.03;
        return this.basePrice * discountFactor;
    }


    get price(){
        const discountFactor = 0.98;
        if(this.basePrice>1000) discountFactor -= 0.03;
        return this.basePrice * discountFactor;
    }


    get price(){
        const discountFactor = this.discountFactor;
        return this.basePrice * discountFactor;
    }
    //게터 추출
    get discountFacter() {
        var discountFactor = 0.98;
        if (this.basePrice > 1000)
            discountFactor -= 0.03;
        return discountFactor;
    }


    get price(){
        return this.basePrice * this.discountFactor;
    }


class Order {
    constructor(quantity, item){
        this._quantity = quantity;
        this._item = item;
    }
    get price(){
        return this.basePrice * this.discountFactor;
    }
    get discountFacter() {
        var discountFactor = 0.98;
        if (this.basePrice > 1000)
            discountFactor -= 0.03;
        return discountFactor;
    }
    get basePrice() {
        return this._quantity * this._item.price;
    }
}

 

 

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

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

ebook-product.kyobobook.co.kr

개요

개발 초기엔 단순한 정보 표현을 위한 숫자나 문자열이 시간이 지남에 따라 이상 단순해지지 않게 변한다.

전화번호의 경우 포매팅, 지역 코드 추출, 뒷자리 추출 암호화 특별한 동작이 필요해질 있다.

 

단순한 출력 이상의 기능이 필요해지는 순간 데이터를 표현하는 전용 클래스를 정의해야 한다.

 

예시



class Order{
    constructor(data){
        this.priority = data.priority;
    }
}
//클라이언트
let highPriorityCount = orders.filter(o => "high" === o.priority
                                        || "rush" === o.priority)
                              .length;


class Order{
    constructor(data){
        this._priority = data.priority;
    }
    //변수부터 캡슐화, 나중에 필드 이름이 변경되도 클라이언트 코드는 유지된다.
    get priority(){return this._priority;}
    set priority(arg){this._priority = arg;}
}


//우선순위 속성을 표현하는 값 클래스 정의
class Priority{
    constructor(value){
        this._value = value;
    }
    //get value() 가 아닌 변환함수 사용
    toString(){return this._value;}
}


class Order{
    constructor(data){
        this._priority = data.priority;
    }
    //정의한 클래스 사용하도록 변경
    get priority(){return this._priority.toString();}
    set priority(aString){this._priority = new Priority(aString);}
}


//우선순위 자체가 아닌 우선순위를 표현하는 문자열 표현
class Order{
    constructor(data){
        this._priority = data.priority;
    }
    get priorityString(){return this._priority.toString();}
    set priority(aString){this._priority = new Priority(aString);}
}
class Priority{
    constructor(value){
        this._value = value;
    }
    toString(){return this._value;}
}
let highPriorityCount = orders.filter(o => "high" === o.priorityString
                                        || "rush" === o.priorityString)
                              .length;

 

추가 리팩터링

//Priority 클래스를 직접 사용하는 것이 좋을 것 같아 추가 개선
class Order{
    constructor(data){
        this._priority = data.priority;
    }
    get priority(){return this._priority;}
    get priorityString(){return this._priority.toString();}
    set priority(aString){this._priority = new Priority(aString);}
}
class Priority{
    constructor(value){
        if(value instanceof Priority) return value;
        if(Priority.legalValues().includes(value)) this._value = value;
        else throw new Error(`<${value}>는 요효하지 않은 우선순위입니다.`);
    }
    toString(){return this._value;}
    get _index(){return Priority.legalValues().findIndex(s=>s===this._value);}
    static legalValues(){return ['low','normal','high','rush'];}
    equals(other){return this._index === other._index;}
    higherThan(other){return this._index > other._index;}
    lowerThan(other){return this._index < other._index;}
    compareTo(other){return this._index - other._index;}
}
let highPriorityCount = orders.filter(o => o.priority.higherThan(new Priority("normal")))
                              .length;

 

 

 

 

 

 

 

 

 

 

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

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

ebook-product.kyobobook.co.kr

개요

가변 데이터는 모두 캡슐화하는 것이 좋다. 그러면 데이터 구조가 언제 수정되는지 파악하기 쉽다. 파악이 쉽다는 것은 데이터 구조를 변경하기도 쉬워진다는 말과 같다.

 

컬렉션을 캡슐화할 흔히하는 실수는 컬렉션을 게터로 그대로 반환하는 것이다. 개발자가 캡슐화를 했다고 착각하지만 컬렉션에 저장된 정보를 클라이언트가 바꿀 수가 있어 캡슐화가 깨진다.

위와 같은 문제를 방지하려면, 컬렉션 요소 추가/삭제 가지 기능을 직접 구현해야 한다.

 

컬렉션을 수정하지 않는 다는 규칙을 정해두고, 사용하는 경우도 있는데 그런 관례같은 것에 의존하는 것보다 컬렉션 복사본을 리턴해 실수로 컬렉션을 수정해도 영향을 여지를 없애는 것이 중요하다.

 

내부 컬렉션을 수정하지 못하게 하는 방법 하나는 컬렉션을 리턴하지 말고, 컬렉션을 가진 클래스가 제공한 컬렉션 제어용 메서드 사용이 있다.

방법의 단점은 표준화된 인터페이스가 아닌 별도 메서드를 쓴다는 점에서 기존 라이브러리와 호환성이 크게 떨어진다.

 

다른 방법으로 읽기 전용 컬렉션을 만들어 리턴하는 것이다.

 

가장 흔한 방식은 컬렉션 복사본을 리턴하는 것이다.

 

여기서 중요한 것은 표준화된 방식에 있다. 가지 방식만 적용해서 사용해야 한다.

 

예시

class Person{
    constructor(name){
        this._name = name;
        this._courses = [];
    }
    get name(){return this._name;}
    get courses(){return this._courses;}
    set courses(arg){this.courses = arg;}
}
class Course {
    constructor(name, isAdvanced){
        this._name = name;
        this._isAdvanced = isAdvanced;
    }
    get name(){return this._name;}
    get isAdvanced(){return this._isAdvanced;}
}
//클라이언트 Person이 제공하는 수업 컬렉션에서 수업 정보 얻음
let numAdvancedCourses;
numAdvancedCourses = aPerson.courses
    .filter(c=>c.isAdvanced)
    .length;


//클라이언트
const basicCourseNames = readBasicCourseNames(filename);
aPerson.courses = basicCourseNames.map(name => new Course(name, false));
//Person에서 게터로 수업컬렉션을 못가져오지만
//내가 수업 컬렉션을 세터로 설정하면, 수업 컬렉션을 수정해 캡슐화가 깨질 수 있다.


class Person{
    constructor(name){
        this._name = name;
        this._courses = [];
    }
    get name(){return this._name;}
    get courses(){return this._courses;}
    set courses(arg){this.courses = arg;}
    //제대로된 캡슐화를 위해 수업추가/제거 메서드 추가
    addCourse(aCourse){
        this._courses.push(aCourse);
    }
    removeCourse(aCourse, fnIfAbsent= () => {throw new RangeError();}){
        const index = this._courses.indexOf(aCourse);
        if(index=== -1) fnIfAbsent();
        else this._courses.splice(index,1);
    }
}


//만들어둔 메서드를 사용해야해서, 이제 이방식으론 저장 못한다.
// aPerson.courses = basicCourseNames.map(name => new Course(name, false));
for(const name of basicCourseNames){
    aPerson.courses.addCourse(new Course(name,false));
}
    //세터를 제거한다. 만약 제공해야한다면 복사본을 저장한다. 권장하는 것은 세터제거
    // set courses(aList){this.courses = _.cloneDeep(aList);}
    //사본을 제공한다.
    get courses(){return _.cloneDeep(this._courses);}
class Person{
    constructor(name){
        this._name = name;
        this._courses = [];
    }
    get name(){return this._name;}
    get courses(){return _.cloneDeep(this._courses);}
    addCourse(aCourse){
        this._courses.push(aCourse);
    }
    removeCourse(aCourse, fnIfAbsent= () => {throw new RangeError();}){
        const index = this._courses.indexOf(aCourse);
        if(index=== -1) fnIfAbsent();
        else this._courses.splice(index,1);
    }
}
class Course {
    constructor(name, isAdvanced){
        this._name = name;
        this._isAdvanced = isAdvanced;
    }
    get name(){return this._name;}
    get isAdvanced(){return this._isAdvanced;}
}
let numAdvancedCourses;
numAdvancedCourses = aPerson.courses
    .filter(c=>c.isAdvanced)
    .length;
const basicCourseNames = readBasicCourseNames(filename);
for(const name of basicCourseNames){
    aPerson.courses.addCourse(new Course(name,false));
}

 

컬렉션은 무조건 복제본을 만드는 것이 예상치 못한 수정으로 인한 버그를 막을 있다.

 

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

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

ebook-product.kyobobook.co.kr

개요

레코드는 프로그래밍에서 데이터를 표현하는 구조이다.

연관된 데이터를 묶어 하나의 의미있는 단위를 만든다.

 

예시: 간단한 레코드 캡슐화하기

//이 상수는 레코드 구조로 사용되는 객체
const organization = {name:"애크미 구스베리", country:"GB"};
let result;
result += `<h1>${organization.name}</h1>` //일기
organization.name = '새로운 이름' // 쓰기
//상수를 캡슐화하기, 이 게터는 임시 사용
function getRawDataOfOrganization() {
    return organization
}
let result;
result += `<h1>${getRawDataOfOrganization().name}</h1>` //일기
getRawDataOfOrganization().name = '새로운 이름' // 쓰기
const organization = new Organization({ name: "애크미 구스베리", country: "GB" });
function getRawDataOfOrganization() {return organization._data;}
function getOrganization(){return organization;}
let result;
result += `<h1>${getRawDataOfOrganization().name}</h1>` //일기
getRawDataOfOrganization().name = '새로운 이름' // 쓰기
//레코드를 캡슐화하는 목적은 변수를 통제하기 위함이다.
//그러기 위해선 레코드를 클래스로 바꿔야한다.
class Organization{
    constructor(data){
        this._data = data;
    }
}


const organization = new Organization({ name: "애크미 구스베리", country: "GB" });
// function getRawDataOfOrganization() {return organization._data;} 정상동작하면 제거
function getOrganization(){return organization;}
let result;
result += `<h1>${getOrganization().name}</h1>` //일기
getOrganization().name = '새로운 이름' // 쓰기
//게터, 세터 생성 후 사용하도록 코드 수정
class Organization{
    constructor(data){
        this._data = data;
    }
    set name(aString){this._data.name = aString;}
    get name(){return this._data.name;}
}
//현재 레코드를 그대로 받아 사용하고 있다.
//레코드는 참조변수로 다루기에 캡슐화가 깨질 우려가 있다. 제거한다.
//만일 _data를 그대로 쓸 것이라면 복제해서 써야한다.
class Organization{
    constructor(data){
        this._name = data.name;
        this._country = data.country;
    }
    set name(aString){this._name = aString;}
    get name(){return this._name;}
    set _country(aString){this._country = aString;}
    get _country(){return this._country;}
}

 

예시: 중첩된 레코드 캡슐화하기

JSON 같은 여러 겹으로 중첩된 레코드에 대한 캡슐화



//중첩구조가 심할수록 데이터 일고/쓰기가 힘들어진다.
customerData[customerID].usages[year][month] = amount;//쓰기
function compareUsage(customerID, laterYear, month){//읽기
    const later = customerData[customerID].usages[laterYear][month];
    const earlier = customerData[customerID].usages[laterYear-1][month];
    return {laterAmount: later, change:later-earlier};
}


getRawDataOfCustomers()[customerID].usages[year][month] = amount;//쓰기
function compareUsage(customerID, laterYear, month){//읽기
    const later = getRawDataOfCustomers()[customerID].usages[laterYear][month];
    const earlier = getRawDataOfCustomers()[customerID].usages[laterYear-1][month];
    return {laterAmount: later, change:later-earlier};
}
//변수 캡슐화부터 시작
function getRawDataOfCustomers(){return customerData;}
function setRawDataOfCustomers(arg){customerData = arg;}


function getCustomerData(){return customerData;}
function getRawDataOfCustomers(){return customerData._data;}//기존 호환
function setRawDataOfCustomers(arg){customerData = new CustomerData(arg);}
//데이터 구조를 표햔하는 클래스 정의 후 이를 반환하는 함수 만듦
class CustomerData{
    constructor(data){
        this._data = data;
    }
}


setUsage(customerID, year, month, amount);//쓰기
//데이터 구조 안으로 들어가는 세터 함수화
function setUsage(customerID, year, month, amount) {
    getRawDataOfCustomers()[customerID].usages[year][month] = amount;
}


getCustomerData().setUsage(customerID, year, month, amount);//쓰기


//클래스로 함수 옮기기
class CustomerData{
    constructor(data){
        this._data = data;
    }
    setUsage(customerID, year, month, amount) {
        getRawDataOfCustomers()[customerID].usages[year][month] = amount;
    }
}


class CustomerData{
    constructor(data){
        this._data = data;
    }
    setUsage(customerID, year, month, amount) {
        getRawDataOfCustomers()[customerID].usages[year][month] = amount;
    }
    //객체의 모든 필드가 불변이 아니라면, 캡슐화가 꺠질 수 있다.
    //그렇다고 customerData 를 사용하는 모든 코드를 확인 했는지 알 수가 없다.
    //복사본을 리턴해서 처리한다.
    get rawData(){
        return _.cloneDeep(this._data);
    }
}


프록시 구현으로 객체 수정 예외를 던지는 방법도 있다.


function compareUsage(customerID, laterYear, month){//읽기
    const later = getCustomerData().usage(customerID,laterYear,month);
    const earlier = getCustomerData().usage(customerID,laterYear-1,month);
    return {laterAmount: later, change:later-earlier};
}


class CustomerData{
    constructor(data){
        this._data = data;
    }
    setUsage(customerID, year, month, amount) {
        getRawDataOfCustomers()[customerID].usages[year][month] = amount;
    }
    get rawData(){
        return _.cloneDeep(this._data);
    }
    //읽기 처리
    usage(customerID, year, month){
        return this._data[customerID].usages[year][month];
    }
}


//사용자가 데이터를 요청해 그 값을 수정하면 캡슐화가 깨질 수 있다.
//복사본으로 처리하는 게 가장 간단한 방식
function compareUsage(customerID, laterYear, month){
    const later = getCustomerData().rawData[customerID].usages[year][month];
    const earlier = getCustomerData().rawData[customerID].usages[year][month];
    return {laterAmount: later, change:later-earlier};
}

요청 데이터를 리턴할 복사본으로 작업하면 간단하나 구조가 거대하면 성능이 감당 안될 수도 있다. 이럴 프록시나 객체를 동결해 값을 비교해 틀어지면 예외를 던지도록 구현하는 방법도 있다.

 

다른 방법으로 레코드 캡슐화를 재귀적으로 하는 방식이 있다. 가장 손이 많이간다.

 

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

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

ebook-product.kyobobook.co.kr

개요

서로 다른 대상을 한꺼번에 다루는 코드를 각각 별개 모듈로 나누는 방업

모듈에만 집중해 다른 모듈을 신경 필요가 없어짐

 

동작을 연이은 단계로 쪼개기

입력 값이 처리 로직에 적합하지 않은 경우, 전처리로 입력값을 처리에 적합한 값으로 바꾼다.

이후 로직을 수행한다.

 

경우 대표적인 예시가 컴파일러다.

언어에 맞는 문법 텍스트를 입력으로 받는다.

텍스트를 토큰화 한다. 토큰은 파싱한다. 구문 트리를 만든다. 최적화 등의 이유로 구문 트리를 변환한다.

마지막으로 바이트 코드(목적코드) 생성한다.

단계는 자신의 단계만 신경 쓰면 된다.

 

예시

상품 결제 금액 계산

//상품 결제 금액 계산 코드
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const shippingPerCase = (basePrice > shippingMethod.discountThreshold )
        ?shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = basePrice - discount + shippingCost;
    return price;
}
//배송비 계산 부분 함수로 추출
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const price = applyShipping(basePrice, shippingMethod, quantity, discount);
    return price;
}
function applyShipping(basePrice, shippingMethod, quantity, discount) {
    const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = basePrice - discount + shippingCost;
    return price;
}


//단계 간 주고받을 중간 데이터 구조
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const priceData = {};
    const price = applyShipping(priceData, basePrice, shippingMethod, quantity, discount);
    return price;
}
function applyShipping(priceData, basePrice, shippingMethod, quantity, discount) {
    const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = basePrice - discount + shippingCost;
    return price;
}


//중간 데이터 구조에 데이터를 넣고 인자를 하나씩 줄여간다.
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const priceData = {basePrice:basePrice};
    const price = applyShipping(priceData, shippingMethod, quantity, discount);
    return price;
}
function applyShipping(priceData, shippingMethod, quantity, discount) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = priceData.basePrice - discount + shippingCost;
    return price;
}


//과정을 반복하며, 중간 데이터 구조 완성
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const priceData = {basePrice:basePrice , quantity : quantity, discount : discount};
    const price = applyShipping(priceData, shippingMethod);
    return price;
}
function applyShipping(priceData, shippingMethod) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = priceData.quantity * shippingPerCase;
    const price = priceData.basePrice - priceData.discount + shippingCost;
    return price;
}


function priceOrder(product, quantity, shippingMethod){
    const priceData = calculatePricingData(product, quantity);
    const price = applyShipping(priceData, shippingMethod);
    return price;
}
//첫 번째 단계 처리 함수
function calculatePricingData(product, quantity) {
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const priceData = { basePrice: basePrice, quantity: quantity, discount: discount };
    return priceData;
}
//두 번째 단계 처리 함수
function applyShipping(priceData, shippingMethod) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = priceData.quantity * shippingPerCase;
    const price = priceData.basePrice - priceData.discount + shippingCost;
    return price;
}


//상수 정리
function priceOrder(product, quantity, shippingMethod){
    const priceData = calculatePricingData(product, quantity);
    return applyShipping(priceData, shippingMethod);
}
function calculatePricingData(product, quantity) {
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    return { basePrice: basePrice, quantity: quantity, discount: discount };
}
function applyShipping(priceData, shippingMethod) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = priceData.quantity * shippingPerCase;
    return priceData.basePrice - priceData.discount + shippingCost;
}

예시 : 명령줄 프로그램 쪼개기(자바)



import java.io.File;
import java.nio.file.Paths;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TestEx {
//현재 두 가지 일을 하고 있다. 하나는 갯수 세기, 다른 하나는 인수가 -r 일 시 ready상태 갯수 세기
    static class Order{
        String status;
    }
    public static void main(String[] args) {
        try {
            if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
            String filename = args[args.length -1];
            File input = Paths.get(filename).toFile();
            ObjectMapper mapper = new ObjectMapper();
            Order[] orders = mapper.readValue(input, Order[].class);
            if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
                System.out.println(Stream.of(orders)
                        .filter(o->"ready".equals(o.status))
                        .count());
            }else {
                System.out.println(orders.length);
            }
        } catch (Exception e) {
            System.err.println(e);
            System.exit(1);
        }
    }
}



//자바 명령줄 프로그램 테스트 시 매번 JVM 구동은 느리니
//JUnit 호출 가능한 상태로 변경
public class TestEx {
    static class Order{
        String status;
    }
    public static void main(String[] args) {
        try {
            System.out.println(run(args));
        } catch (Exception e) {
            System.err.println(e);
            System.exit(1);
        }
    }
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        String filename = args[args.length -1];
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }
}


    //먼저, 두 번째 단계에 해당하는 코드를 메서드로 추출
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        String filename = args[args.length -1];
        return countOrders(args, filename);
    }
    private static long countOrders(String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //중간 데이터 구조 추가
    private static class CommandLine{}
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        String filename = args[args.length -1];
        return countOrders(commandLine, args, filename);
    }
    private static long countOrders(CommandLine commandLine, String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //두 번째 단계로 가는 인수 분석하기
    private static class CommandLine{}
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        String filename = args[args.length -1];
        return countOrders(commandLine, args, filename);
    }
    private static long countOrders(CommandLine commandLine, String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        //args 는 두 번째 단계에서 굳이 필요없다. 이를 위해 먼저 조건식을 변수로 추출
        boolean onlyCountReady = Stream.of(args).anyMatch(arg->"-r".equals(arg));
        if(onlyCountReady) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    private static class CommandLine{
        private boolean onlyCountReady;
    }
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        String filename = args[args.length -1];
        //중간 데이토 구조로 옮김, 이후 첫번째 단계로 옮김, 이후 매개변수 제거
        commandLine.onlyCountReady = Stream.of(args).anyMatch(arg->"-r".equals(arg));
        return countOrders(commandLine, filename);
    }
    private static long countOrders(CommandLine commandLine, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(commandLine.onlyCountReady) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //잔여 매개변수 처리 완료
    private static class CommandLine{
        private boolean onlyCountReady;
        private String filename;
    }
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        commandLine.filename = args[args.length -1];
        commandLine.onlyCountReady = Stream.of(args).anyMatch(arg->"-r".equals(arg));
        return countOrders(commandLine);
    }
    private static long countOrders(CommandLine commandLine)
            throws IOException {
        File input = Paths.get(commandLine.filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(commandLine.onlyCountReady) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //메인 작업 완료, 잔여 코드 정리(리팩터링)
    private static class CommandLine{
        private boolean onlyCountReady;
        private String filename;
    }
    static long run(String[] args) throws IOException{
        return countOrders(parseCommandLine(args));
    }
    private static CommandLine parseCommandLine(String[] args) {
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine result = new CommandLine();
        result.filename = args[args.length -1];
        result.onlyCountReady = Stream.of(args).anyMatch(arg->"-r".equals(arg));
        return result;
    }
    private static long countOrders(CommandLine commandLine)
            throws IOException {
        File input = Paths.get(commandLine.filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(commandLine.onlyCountReady) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }

예시 : 번째 단계에 변환기 사용하기(자바)

이전 예시와 다르게 데이터 구조 생성 전달이 아닌 번째 단계에 적합한 인터페이스로 바꿔주는 변환기 객체 사용

 



    //중간 데이터 구조 추가 부분부터
    private static class CommandLine{}
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        String filename = args[args.length -1];
        return countOrders(commandLine, args, filename);
    }
    private static long countOrders(CommandLine commandLine, String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //최상위 별도 클래스 파일로 뺏다고 가정
    //CommandLine.java
    private static class CommandLine{
        String[] args;
        public CommandLine(String[] args) {
            this.args = args;
        }
    }
    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        String filename = args[args.length -1];
        return countOrders(commandLine, args, filename);
    }
    private static long countOrders(CommandLine commandLine, String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine, args, filename(args));
    }
    //임시변수를 질의함수로 바꾸기
    private static String filename(String[] args) {
        return args[args.length -1];
    }


    //CommandLine.java
    private static class CommandLine{
        String[] args;
        public CommandLine(String[] args) {
            this.args = args;
        }
        //질의 함수 옮기기
        public  String filename() {
            return args[args.length -1];
        }
    }
    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine, args, commandLine.filename());
    }


    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine, args);
    }
    //매개변수 정리
    private static long countOrders(CommandLine commandLine, String[] args)
            throws IOException {
        File input = Paths.get(commandLine.filename()).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //CommandLine.java
    private static class CommandLine{
        String[] args;
        public CommandLine(String[] args) {
            this.args = args;
        }
        public  String filename() {
            return args[args.length -1];
        }
        //함수 추출 후, 클래스로 옮김 이후, 불필요한 인자 제거
        public boolean onlyCountReady() {
            return Stream.of(args).anyMatch(arg->"-r".equals(arg));
        }
    }
    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine);
    }
    private static long countOrders(CommandLine commandLine)
            throws IOException {
        File input = Paths.get(commandLine.filename()).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(commandLine.onlyCountReady()) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //CommandLine.java
    private static class CommandLine{
        String[] args;
        public CommandLine(String[] args) {
            //검사로직 옮기기, 리팩터링 완료
            if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
            this.args = args;
        }
        public  String filename() {
            return args[args.length -1];
        }
        public boolean onlyCountReady() {
            return Stream.of(args).anyMatch(arg->"-r".equals(arg));
        }
    }
    //Main.java
    static long run(String[] args) throws IOException{
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine);
    }

 

명령줄 프로그램 쪼개기를 번째 단계에 해당하는 코드를 독립 함수로 추출한 중간 데이터 구조를 구조체로 쓸지, 변환기를 쓸지는 상관없다.

핵심은 단계를 명확히 분리하는

 

 

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

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

ebook-product.kyobobook.co.kr

개요

소프트웨어는 데이터를 입력받아서 여러 가지 정보를 도출하기도 한다.

도출 정보는 여러 곳에서 사용된다. 이런 반복 도출 작업을 곳에 모아두면 중복을 막을 있다.

 

방법으로 변환 함수가 있다.

원본 데이터를 입력받아 필요한 정보를 모두 도출한 , 각각 출력 데이터의 필드에 넣어 반환한다.

 

리팩터링 대신 "여러 함수를 클래스로 묶기" 처리해도 된다.

 

원본 데이터가 코드 안에서 갱신될 때는 클래스로 묶는 편이 데이터 일관성 유지 때문에 좋다.

변환 함수는 가공한 데이터를 새로운 레코드에 저장하기에 일관성이 깨질 있다.

 

여러 함수를 묶는 이유는 로직 중복 회피다.

 

예시

차를 수돗물 처럼 제공하는 서비스 제공을 가정한다.

수돗물처럼 계량해 비용을 산정해야 한다.

//차 사용량 레코드
reading = {customer: "ivan", quantity:10, month: 5, year:2017};


//client1.js  기본 요금 계산 코드
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) *aReading.quantity;


//client2.js  차 세금 일부 면제 코드
const aReading = acquireReading();
const base = (baseRate(aReading.month, aReading.year) *aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));


/** client3.js
 * 중복코드가 한 곳에 몰려있으면 함수 추출로 제거하면 된다.
 * 현재 중복 코드가 전체에 흩어져 있다고 가정한다.
 */
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);
//이미 함수로 만든 코드가 발견됐다.
function calculateBaseCharge(aReading){
    return baseRate(aReading.month, aReading.year) * aReading.quantity;
}
//먼저 입력 객체를 그대로 복사해 반환하는 함수를 만든다.
function enrichReading(original){
    const result = _.cloneDeep(original);
    return result;
}
const rawReading = acquireReading(); //미가공 측정값
const aReading = enrichReading(rawReading);
const basicChargeAmount = calculateBaseCharge(aReading);
function enrichReading(original){
    const result = _.cloneDeep(original);
    //미가공 측정값에 기본 소비량을 부가 정보로 덧붙임
    result.baseCharge =  calculateBaseCharge(result );
    return result;
}
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = aReading.baseCharge;
//enrichReading() 함수가 정보추가 시 원본 함수를 훼손하는지 체크
it('check reading unchanged', function(){
    const baseReading = reading = {customer: "ivan", quantity:10, month: 5, year:2017};
    const oracle = _.cloneDeep(baseReading);
    enrichReading(baseReading);
    assert.deepEquals(baseReading, oracle);
})
//client1도 수정
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const baseCharge = aReading.baseCharge;
//client2.js  차 세금 일부 면제 코드
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const base = aReading.baseCharge;
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));
function enrichReading(original){
    const result = _.cloneDeep(original);
    result.baseCharge =  calculateBaseCharge(result);
    //함수 이동
    result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
    return result;
}


//client2.js  차 세금 일부 면제 코드
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = aReading.taxableCharge;
//전체
reading = {customer: "ivan", quantity:10, month: 5, year:2017};
//client1
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const baseCharge = aReading.baseCharge;
//client2.js  차 세금 일부 면제 코드
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = aReading.taxableCharge;
//client3
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = aReading.baseCharge;
function enrichReading(original){
    const result = _.cloneDeep(original);
    result.baseCharge =  calculateBaseCharge(result);
    //함수 이동
    result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
    return result;
}
//이미 함수로 만든 코드가 발견됐다.
function calculateBaseCharge(aReading){
    return baseRate(aReading.month, aReading.year) * aReading.quantity;
}
//enrichReading() 함수가 정보추가 시 원본 함수를 훼손하는지 체크
it('check reading unchanged', function(){
    const baseReading = reading = {customer: "ivan", quantity:10, month: 5, year:2017};
    const oracle = _.cloneDeep(baseReading);
    enrichReading(baseReading);
    assert.deepEquals(baseReading, oracle);
})

 

측정값에 부가 정보를 추가하는 지금 방식은 클라이언트가 데이터를 변경하면 데이터 일관성이 깨져 심각한 문제가 생길 있다.

자바스크립트에서 이런 문제를 방지하기 좋은 방법은 여러 함수를 클래스로 묶기다.

불변 데이터 구조를 지원하는 언어라면 이런 문제가 생길 일이 없어 여러 함수를 변환 함수로 묶기를 병용해서 사용해도 괜찮다.

불변성을 제공하지 않은 언어라도 데이터를 읽기전용으로 사용될 때는 변환 함수 사용도 괜찮다.

 

+ Recent posts