리팩터링 | 마틴 파울러 | 한빛미디어- 교보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);
})

 

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

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

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

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

 

 

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

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

ebook-product.kyobobook.co.kr

개요

클래스는 대다수 언어가 제공하는 기본적인 빌딩 블록

클래스는 데이터와 함수를 하나의 공유 환경으로 묶어 그중 일부를 외부에 제공한다.

 

함수 호출 인수로 전달되는 공통 데이터를 중심으로 작동하는 함수들을 클래스 하나로 묶는다.

클래스로 묶으면 함수들 공통 환경을 명확히 표현할 있다.

 

클래스로 묶을 장점은 클라이언트가 객체의 핵심 데이터를 변경할 있고, 파생 객체들을 일관되게 관리할 있다.

 

함수들을 중첩함수로 묶는도 되지만, 클래스를 지원하는 언어라면 클래스로 묶는다.

만약 클래스를 지원하지 않는 언어라는 '함수를 객체처럼 패턴' 사용을 고려한다.

//함수를 객체처럼
function Cat(name){
    return {
        name : name,
        getName : function(){return name;},
        setName : function(arg){name=arg},
        sound : function(){sound(name);}
    };
    function sound(name){console.log(name + " 냐아아아옹");}
}
var cat = Cat('나비');
cat.sound();
cat.setName('배추나비');
cat.sound();
///
나비 냐아아아옹
배추나비 냐아아아옹

 

예시

마시는 차를 정부에서 수돗물처럼 제공한다고 가정, 계량기를 읽어서 측정값을 기록 계산하는 코드

//측정값
let reading={customer : "ivan", quantity:10, month:5, uear:2017};
//클라이언트1
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
//클라이언트2
const aReading2 = acquireReading();
const baseCharge2 = baseRate(aReading2.month, aReading2.year) * aReading2.quantity;
const taxableCharge2 = Math.max(0,base-taxTreshold(aReading2.year));
//클라이언트3
const aReading3 = acquireReading();
const basicChargeAmount3 = calculateBaseCharge(aReading3);
function calculateBaseCharge(aReading){
    return baseRate(aReading.month, aReading.year);
}
//레코드를 클래스로 캡슐화
class Reading{
    constructor(data){
        this._customer = data.customer;
        this._quantity = data.quantity;
        this._month = data.month;
        this._year = data.year;
    }
    get customer(){return this._customer;}
    get quantity(){return this._quantity;}
    get month(){return this._month;}
    get year(){return this._year;}
}


//클라이언트3
const rawReading = acquireReading();
const aReading3 = new Reading(rawReading);
const basicChargeAmount = aReading3.calculateBaseCharge;
//레코드를 클래스로 캡슐화
class Reading{
    constructor(data){
        this._customer = data.customer;
        this._quantity = data.quantity;
        this._month = data.month;
        this._year = data.year;
    }
    get customer(){return this._customer;}
    get quantity(){return this._quantity;}
    get month(){return this._month;}
    get year(){return this._year;}
    //함수 옮김
    get calculateBaseCharge(){
        return baseRate(this.month, this.year);
    }
}
const rawReading = acquireReading();
const aReading3 = new Reading(rawReading);
const basicChargeAmount = aReading3.baseCharge;
class Reading{
    constructor(data){
        this._customer = data.customer;
        this._quantity = data.quantity;
        this._month = data.month;
        this._year = data.year;
    }
    get customer(){return this._customer;}
    get quantity(){return this._quantity;}
    get month(){return this._month;}
    get year(){return this._year;}
    //알맞은 이름 짓기
    get baseCharge(){
        return baseRate(this.month, this.year);
    }
}
//클라이언트1
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const baseCharge = aReading.baseRate;


//클라이언트2
const rawReading2 = acquireReading();
const aReading2 = new Reading(rawReading);
const taxableCharge2 = Math.max(0,aReading2.baseRate-taxTreshold(aReading2.year));


//클라이언트는 aReading.baseRate  이렇게 호출한다.
이게 필드인지 함수호출로 계산된 값인지 구분할 수없다. 이를 단일 접근 원칙이라 한다.
//클라이언트2
const rawReading2 = acquireReading();
const aReading2 = new Reading(rawReading);
const taxableCharge2 = taxableChargeFn() ;
//함수로 추출
function taxableChargeFn() {
    return Math.max(0, aReading2.baseRate - taxTreshold(aReading2.year));
}


//측정값
let reading={customer : "ivan", quantity:10, month:5, uear:2017};
//클라이언트1
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const baseCharge = aReading.baseRate;
//클라이언트2
const rawReading2 = acquireReading();
const aReading2 = new Reading(rawReading);
const taxableCharge2 = aReading2.taxableCharge;
//클라이언트3
const rawReading3 = acquireReading();
const aReading3 = new Reading(rawReading3);
const basicChargeAmount = aReading3.baseCharge;
class Reading{
    constructor(data){
        this._customer = data.customer;
        this._quantity = data.quantity;
        this._month = data.month;
        this._year = data.year;
    }
    get customer(){return this._customer;}
    get quantity(){return this._quantity;}
    get month(){return this._month;}
    get year(){return this._year;}
    get baseCharge(){
        return baseRate(this.month, this.year);
    }
    get taxableCharge() {
        return Math.max(0, this.baseRate - taxTreshold(aReading2.year));
    }
}
 

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

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

ebook-product.kyobobook.co.kr

개요

명확한 프로그래밍의 핵심은 이름짓기.

자바스크립트 같은 동적 타입 언어라면 앞에 타입을 명시하는 식으로 이름 짓는 것도 좋다. (String name ==> let strName)

예시

가장 간단한 바꾸기는 유효범위가 좁은 변수다.

let tpHd = "untitled";
//참조하는 곳
result += `<h1>${tpHd}</h1>`;
obj['articleTitle'] = 'Hello World';
//바꾸는 곳
tpHd = obj['articleTitle'];
let tpHd = "untitled";
result += `<h1>${title()}</h1>`;
obj['articleTitle'] = 'Hello World';
setTitle(obj['articleTitle']);
//캡슐화
//게터
function title(){ return tpHd;}
//세터
function setTitle(arg){tpHd = arg;}
let 이제변수이름바꿔도됨 = "untitled";
result += `<h1>${title()}</h1>`;
obj['articleTitle'] = 'Hello World';
setTitle(obj['articleTitle']);
function title(){ return 이제변수이름바꿔도됨;}
function setTitle(arg){이제변수이름바꿔도됨 = arg;}
이름 변경 다시 함수를 인라인해도 된다. , 캡슐화한 변수가 전역으로 두루 사용된다면, 나중을 위해 함수로 두는 것이 좋다.

 

예시:상수 이름 바꾸기

상수의 이름은 캡슐화하지 않고 복제 방식으로 점진적으로 바꿀 있다.

const cpyNm = "애크미 구스베리";
//원본 이름 바꾼 후 이전 원본 이름에 바꾼 이름 복제본 대입
const companyName = "애크미 구스베리";
const cpyNm = companyName;

방식은 상수 포함. 클라이언트가 읽기전용인 변수에도 적용할 있다.

자바스크립트 익스포트한 변수

 

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

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

ebook-product.kyobobook.co.kr

개요

함수 함수 사이를 같이 다니는 여러 데이터 항목을 하나의 데이터 구조로 묶는다.

 

데이터 뭉치를 데이터 구조로 묶으면 데이터 사이 관계가 명확해진다.

그리고 함수 인자 수가 줄어든다.

묶인 데이터는 이름을 가지기 때문에 인자로 사용 일관성을 가진다.

 

생성된 매개변수 객체는 새로운 리팩터링 기회를 만들 있다.

 

예시

const station = {
    name: "ZB1",
    readings: [
        {temp:47, time: "2016-11-10 09:10"},
        {temp:53, time: "2016-11-10 09:20"},
        {temp:58, time: "2016-11-10 09:30"},
        {temp:53, time: "2016-11-10 09:40"},
        {temp:51, time: "2016-11-10 09:50"},
    ]
};
//정상 범위를 벗어난 측정값 탐색
function readingsOutsideRange(station, min, max){
    return station.readings
        .filter(r => r.temp < min || r.temp > max);
}
//호출
let alerts = readingsOutsideRange(station,
     operationPlan.temperatureFloor,
    operationPlan.temperatureCeiling);
//묶을 클래스
class NumberRange{
    constructor(min,max){
        this._data = {min:min, max:max};
    }
    get min(){return this._data.min};
    get max(){return this._data.max};
}
//새로운 데이터 구조 매개변수 추가
function readingsOutsideRange(station, min, max, range){
    return station.readings
        .filter(r => r.temp < min || r.temp > max);
}
let alerts = readingsOutsideRange(station,
     operationPlan.temperatureFloor,
    operationPlan.temperatureCeiling
    ,null);//일단 null
//점진적으로 하나씩 줄여가며 테스트 한다.
function readingsOutsideRange(station, min, /*max,*/ range){
    return station.readings
        .filter(r => r.temp < min || r.temp > range.max);
}
const range = new NumberRange(operationPlan.temperatureFloor,
                             operationPlan.temperatureCeiling);
let alerts = readingsOutsideRange(station,
     operationPlan.temperatureFloor,
    // operationPlan.temperatureCeiling,
    range);
 // 완료
const station = {
    name: "ZB1",
    readings: [
        {temp:47, time: "2016-11-10 09:10"},
        {temp:53, time: "2016-11-10 09:20"},
        {temp:58, time: "2016-11-10 09:30"},
        {temp:53, time: "2016-11-10 09:40"},
        {temp:51, time: "2016-11-10 09:50"},
    ]
};
function readingsOutsideRange(station, range){
    return station.readings
        .filter(r => r.temp < range.min || r.temp > range.max);
}
const range = new NumberRange(operationPlan.temperatureFloor,
                             operationPlan.temperatureCeiling);
let alerts = readingsOutsideRange(station, range);
class NumberRange{
    constructor(min,max){
        this._data = {min:min, max:max};
    }
    get min(){return this._data.min};
    get max(){return this._data.max};
}
//연관된 데이터를 묶음으로서 새로운 설계의 기회를 얻는다.
class NumberRange{
    constructor(min,max){
        this._data = {min:min, max:max};
    }
    get min(){return this._data.min;}
    get max(){return this._data.max;}
    contains(arg){return arg>=this.min&&arg<=this.max;}
}

 

 

 

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

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

ebook-product.kyobobook.co.kr

개요

리팩터링은 결국 프로그램의 요소를 조작하는 , 조작은 변수보다 함수가 쉽다.

함수는 변경 기존 함수에 전달 함수를 활용해 임시 버퍼 역할을 수도 있어 유연하다. (차후 기존함수명으로 변경)

 

반면에 변수는 번에 바꿔야 해서 까다롭다.

변수의 유효 범위가 좁으면 문제가 거의 없지만 넓어질수록 문제가 된다.

 

넓은 범위 변수는 함수로 데이터를 캡슐화하는 것이 가장 좋은 방법이다.

데이터 재구성 ==> 함수 재구성으로 변환

 

함수는 동작이기에 중간에 검증 같은 조직을 끼워 넣을 있다.

 

이러한 이유가 객체지향에서 객체 데이터를 최대한 private 유지하는 이유다. (데이터에 대한 결합도를 떨어트리기 위함)

 

불변 데이터는 가변 데이터보다 캡슐화할 이유가 적다. 데이터가 변경될 일이 없어서 그냥 복사해서 사용하면 된다.

예시

let spaceship = {};
let defaultOwner = {firstName:"마틴",lastName:"파울러"};
//참조하는 코드
spaceship.owner = defaultOwner;
//갱신하는 코드(불변이라면 갱신 걱정이 없다)
defaultOwner = {firstName:"레베카",lastName:"파슨스"};
let spaceship = {};
let defaultOwner = {firstName:"마틴",lastName:"파울러"};
spaceship.owner = defaultOwner;
defaultOwner = {firstName:"레베카",lastName:"파슨스"};
//게터/세터 생성
function getDefaultOwner(){return defaultOwner;}
function setDefaultOwner(arg){return defaultOwner = arg;}
let spaceship = {};
let defaultOwner = {firstName:"마틴",lastName:"파울러"};
//게터 호출, 대입문은 세터 호출
spaceship.owner = getDefaultOwner();
setDefaultOwner({firstName:"레베카",lastName:"파슨스"});
function getDefaultOwner(){return defaultOwner;}
function setDefaultOwner(arg){return defaultOwner = arg;}
let spaceship = {};
//접근 제한자가 없는 언어에서는 이런 식으로 표현하는 것도 도움된다.
let __privateOnly_defaultOwner = {firstName:"마틴",lastName:"파울러"};
spaceship.owner = getDefaultOwner();
setDefaultOwner({firstName:"레베카",lastName:"파슨스"});
//저자는 자바스크립트에서는 게터에 get을 빼는 것을 선호함
function defaultOwner(){return __privateOnly_defaultOwner;}
function setDefaultOwner(arg){return __privateOnly_defaultOwner = arg;}

 

자바스크립트에선 게터와 세터 이름을 똑같이 짓고 인수 존재 여부로 구분하는 방식을 많이 쓰기때문에 get

 

캡슐화하기

//test_refac.mjs
import {defaultOwner} from './test_refac.mjs';
import { strict as assert } from 'node:assert';
const owner1 = defaultOwner();
assert.equal("파울러",owner1.lastName,"처음 값 확인");
const owner2 = defaultOwner();
owner2.lastName ="파슨스";
assert.equal("파슨스",owner2.lastName,"owner2 변경 후");
import {defaultOwner} from './test_refac.mjs';
import { strict as assert } from 'node:assert';
const owner1 = defaultOwner();
assert.equal("파울러",owner1.lastName,"처음 값 확인");
const owner2 = defaultOwner();
owner2.lastName ="파슨스";
console.log(owner1);
assert.equal("파슨스",owner1.lastName,"owner2 변경 후");
console.log(owner1);
////// 객체 값이 변경된 것을 볼 수 있다.
Debugger attached.
{ firstName: '마틴', lastName: '파슨스' }
{ firstName: '마틴', lastName: '파슨스' }
Waiting for the debugger to disconnect…
let spaceship = {};
let defaultOwnerData = {firstName:"마틴",lastName:"파울러"};
spaceship.owner = defaultOwner();
//방법1 게터에서 객체 복사본을 리턴한다.
export function defaultOwner(){return Object.assign({},defaultOwnerData);}
export function setDefaultOwner(arg){defaultOwnerData = arg;}
결과


클라이언트에서 오히려 원본을 원하는 경우를 주의해야 한다.
let defaultOwnerData = {firstName:"마틴",lastName:"파울러"};
//방법 2 레코드 캡슐화하기
export function defaultOwner(){return new Person(defaultOwnerData);}
export function setDefaultOwner(arg){defaultOwnerData = arg;}
class Person{
    constructor(data){
        this._lastName = data.lastName;
        this._firstName = data.firstName;
    }
    get lastName(){return this._lastName;}
    get firstName(){return this._firstName;}
}

경우에 따라 세터도 복사본을 저장하는게 좋을 수도 있다.

 

복제가 성능에 주는 영향을 미미하다. 반면에 원본을 그대로 사용하면 나중에 디버깅하기 어렵고 시간도 오래 걸릴 위험이 있다.

 

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

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

ebook-product.kyobobook.co.kr

개요

함수는 프로그램을 작은 부분으로 나누는 주된 수단이다.

이런 함수 선언부는 소프트웨어의 연결부 역할을 한다. 따라서 선언부를 정의할 필요가 있다.

 

가장 중요한 함수 이름이다. 이름이 좋으면 구현부를 필요가 없다.

이름은 IDE기능으로 쉽게 변경가능하니 좋은 이름이 있다면 주저하지 않고 바꾸자

 

함수 매개변수는 함수를 사용하는 문맥을 설정한다.

전화번호 포맷팅 함수가 있는데 매개변수로 사람 타입을 받아 전화번호를 포맷팅한다고 하면, 활용도가 적다. 전화번호만 인자로 받는다면 어떤 전화번호라도 활용이 가능하다.

절차

간단한 절차와 마이그레이션 절차가 있다.

간단한 절차를 최대한 사용하되 불가능하다면 마이그레이션 절차를 사용한다.

 

간단한 절차

  1. 매개변수를 제거하려거든 먼저 함수 본문에서 제거 대상 매개변수를 참조하는 곳은 없는지 확인한다.
  2. 메서드 선언을 원하는 형태로 바꾼다.
  3. 기존 메서드 선언을 참조하는 부분을 모두 찾아서 바뀐 형태로 수정한다.
  4. 테스트한다.

 

마이그레이션 절차

  1. 이어지는 추출 단계를 수월하게 만들어야 한다면 함수의 본문을 적절히 리팩터링한다.
  2. 함수 본문을 새로운 함수로 추출한다.
  3. 추출한 함수에 매개변수를 추가해야 한다면 '간단한 절차' 따라 추가한다.
  4. 테스트한다.
  5. 기존 함수를 인라인한다.
  6. 이름을 임시로 붙여뒀다면 함수 선언 바꾸기를 적용해서 원래 이름으로 되돌린다.
  7. 테스트한다.

 

상속 구조 속에 있는 클래스 메서드 변경할 때는 다른 서브 클래스들에도 변경이 반영되어야 한다. 이때 복잡하기에 간접 호출 방식으로 우회하는 방법도 쓴다.

타이핑 처럼 슈퍼클래스와의 연결을 제공하지 않는 언어라면 전달 메서드를 모든 구현 클래스에 추가해야 한다.

 

 

 

예시 : 함수 이름 바꾸기 간단한 절차

function circum(radius){
    return 2 * Math.PI * radius;
}
function circumference(radius){
    return 2 * Math.PI * radius;
}

 

함수 이름 변경 핵심은 함수를 호출하는 부분을 얼마나 쉽게 찾는가에 있다.

정적 타입 언어와 좋은 IDE 조합이라면 쉽게 함수 이름 바꾸기를 처리할 있다.

정적 타입 언어가 아니라면 상당히 귀찮아지고 버그 발생 가능성이 높아진다.

추가로 상속관계에 있는 클래스라면 다른 메서드도 수정해야 된다.

 

리팩터링은 항상 문제를 작게 쪼개서 정복한다.

매개변수 추가/삭제 같은 변경은 반드시 함수 호출 부분을 모두 찾아 수정한 수행한다.

 

간단한 방식은 함수 이름 바꾸기로 코드에 영향이 적을 적합하다.

 

예시: 함수 이름 바꾸기 마이그레이션 절차

function circum(radius){
    return 2 * Math.PI * radius;
}
//마이그레이션 절차
function circum(radius){
    return circumference(radius);
}
function circumference(radius) {
    return 2 * Math.PI * radius;
}
상태에서 정상 동작을 테스트하고,  circum()호출하는 부분을 전부 찾아 circumference() 바꾼다.

 

방식은 API같은 선언부를 고칠 없는 외부 코드 사용 부분을 리팩터링 하기 좋다.

circumference() 만들어 두고, circum() @Deprecated 같은 표시를 해두어 circumference() 호출하도록 유도를 한다. 이후 모든 클라이언트가 변경 했으면 circum() 지운다.

 

 

 

예시: 매개변수 추가하기

예약 우선순위 큐를 지원하라는 요구사항이 들어왔다.

기존 큐와 우선순위 어떤 것으로 할지 결정 매개변수를 추가해야 한다.

 

class Book{
    addReservation(customer){
        this._reservations.push(customer);
    }
}
class Book{
    addReservation(customer){
        this.zz_addReservation(customer);
    }
    //임시이름
    zz_addReservation(customer) {
        this._reservations.push(customer);
    }
}
class Book{
    addReservation(customer){
        this.zz_addReservation(customer, false);
    }
    //매개변수 추가
    zz_addReservation(customer,isPriority) {
        //자바스크립트 한정 assert 테스트
        assert(isPriority === true || isPriority === false);
        this._reservations.push(customer);
    }
}
이제 호출 부분을 전부 zz_addReservation() 변경 zz_addReservation() 함수 이름을 기존 함수 이름으로 바꾼다.
class Book{
    addReservation(customer,isPriority) {
        this._reservations.push(customer);
    }
}

 

 

예시: 매개변수를 속성으로 바꾸기

고객이 뉴잉글랜드 살고있는지 파악하는 함수

/* 현재 문제는 매개변수를 객체로 받고 있다. 재사용성이 제한된다.*/
function inNewEngland(aCustomer){
    return ["MA","CT","ME","VT","NH","RI"].includes(aCustomer.address.state);
}
//호출부
const newEnglanders = someCustomers.filter(c=>inNewEngland(c));
function inNewEngland(aCustomer){
    const stateCode = aCustomer.address.state;
    return ["MA","CT","ME","VT","NH","RI"].includes(stateCode);
}
function inNewEngland(aCustomer){
    const stateCode = aCustomer.address.state;
    return newFunction(stateCode);
}
//임시 함수 추출
function newFunction(stateCode) {
    return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}
function inNewEngland(aCustomer){
    //변수 인라인하기
    return newFunction(aCustomer.address.state);
}
function newFunction(stateCode) {
    return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}
//호출부 변경
const newEnglanders = someCustomers.filter(c=>newFunction(c));
//함수 선언 바꾸기로 새함수이름을 기존함수이름으로 변경
function inNewEngland(stateCode) {
    return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}
//호출부 변경
const newEnglanders = someCustomers.filter(c=>inNewEngland(c));

 

+ Recent posts