refactoring

여러 함수를 변환 함수로 묶기, 단계 쪼개기

단점이없어지고싶은개발자 2022. 8. 2. 22:52
반응형

208p ~ 220p

여러 함수를 변환 함수로 묶기

function base(aReading) {...}
function taxableCharge(aReading) {...}
//-----------------------------------
function enichReading(argReading) {
    const aReading = _.cloneDeep(argReading);
    aReading.baseCharge = base(aReading);
    aReading.taxableCharge = texableCharge(aReading);
    return aReading;
}

소프트웨어는 데이터를 입력받아서 여러 가지 정보를 도출하곤 한다. 이렇게 도출된 정보는 여러 곳에서 사용될 수 있는데, 그러다 보면 이 정보가 사용되는 곳마다 같은 도출 로직이 반복되기도 한다. 이 도출 작업들을 모아두면 검색과 갱신을 일관된 장소에서 처리할 수 있고 로직 중복도 막을 수 있다.

방법으로는 변환 함수(transform)을 사용할 수 있다. 

변환함수란 원본 데이터를 입력받아서 필요한 정보를 모두 도출한 뒤, 각각을 출력 데이터의 필드에 넣어 반환

그래서 도출 과정을 검토할 일이 생기면 변환 함수만 살피면 된다.

 

절차 

1. 변환할 레코드를 입력받아서 값을 그대로 반환하는 변환 함수를 만든다. 

 => 대체로 깊은 복사로 처리해야 한다. 변환 함수가 원본 레코드를 바꾸지 않는지 검사하는 테스트를 만련해두면 도움될 때가 많다.

2. 묶을 함수 중 함수 하나를 골라서 본문 코드를 변환 함수로 옮기고, 처리 결과를 레코드에 새 필드로 기록한다. 그런 다음 클라이언트 코드가 이 필드를 사용하도록 수정한다.

3. 테스트한다.

4. 나머지 관련 함수도 위 과정에 따라 처리한다.

 

예시 : 매달 사용자가 마신 차의 양을 측정해야 한다.

const reading = {
    customer: "ivan",
    quantity: 10,
    month: 5,
    year: 2017
};

사용자에게 요금을 부과하기 위해 기본요금을 계산하는 코드

const aReading = acqireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;

세금을 부과할 소비량을 계산하는 코드

const aReading = acquireReading();
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

위와 같은 코드가 여러 곳에서 반복된다고 해보자. 중복 코드는 나중에 로직을 수정할 때 힘들어진다. 

const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);

function calculateBaseCharge(aReading) { // 다른 곳에서 이미 함수로 만들어둠
    return baseRate(aReading.month, aReading.year) * aReading.quantity;
}

이를 해결하는 방법으로, 다양한 파생 정보 계산 로직을 모두 하나의 변환 단계로 모을 수 있다. 변환 단계에서 미가공 측정값을 입력받아서 다양한 가공 정보를 덧붙여 반환하는 것이다.

function enrichReading(original) {
//깊은 복사는 cloneDeep()으로 처리
    const result = _.cloneDeep(original);
    return result;
}

2. 이제 변경하려는 계산 로직 중 하나를 고른다. 

const rawReading = acquireReading(); //가공 전 측정 값
const aReading = enrichReading(rawReading);
const basicChargeAmount = calculateBaseCharge(aReading);

function enrichReading(original) {
//깊은 복사는 cloneDeep()으로 처리
    const result = _.cloneDeep(original);
    result.baseCharge = calculateBaseCharge(result);
    return result;
}

이어서 이 함수를 사용하던 클라이언트가 부가 정보를 담은 필드를 사용하도록 수정한다.

const rawReading = acquireReading(); //가공 전 측정 값
const aReading = enrichReading(rawReading);
const basicChargeAmount = aReading.baseCharge;

이렇게 되면 기본요금을 이용하는 클라이언트는 변환된 레코드를 사용해야 한다는 의도를 명확히 표현하게 된다.

주의할 점은 rnrichReading()처럼 정보를 추가해 반환할 때 원본 측정값 레코드는 변경하지 않아야 된다.

4. 세금을 부과할 소비량 계산 코드

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshould(aReading.year));
function enrichReading(original) {
    const result = _.cloneDeep(original);
    result.baseCharge = calculateBaseCharge(result);
    result.taxableCharge = Math.max(0, result.basrCharge - taxThreshould(result.yrear));
    return result;
}

새로 만든 필드를 사용하도록 원본 코드를 수정한다

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = aReading.taxableCharge;

 

단계 쪼개기

const orderData = orderString.split(/\s+/);
const productPrice = priceList[orderData[0].split("-")[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;
const orderRecord = parseOrder(order);
const orderPrice = price(orderRecord, priceList);

function parseOrder(String) {
    const values = string.split(/\s+/);
    return ({
        productID : values[0].split("_")][1],
        quantity : parseInt(values[1]),
    });
}

function price(order, priceList) {
    return order.quantity * priceList[order.productID];
}

서로 다른 두 대상을 한 꺼번에 다루는 코드를 발견하면 각각을 별개 모듈로 나누는 방법을 모색한다. 코드를 수정해야 할 때 두 대상을 동시에 생각하는 것이 아닌 하나에만 집중할 수 있기 때문이다. 모듈이 잘 분리되어 있다면 다른 모듈의 상세 내용의 모든 부분을 들여다보지 않아도 된다.

분리하는 가장 편한 방법 중 하나는 동작은 두 단계로 쪼개는 것이다. 가장 대표적인 예는 컴파일러다. 컴파일러는 기본적으로 어떤 텍스트(프로그래밍 언어로 작성된 코드)를 입력받아서 실행 가능한 형태로 변환한다. 즉, 텍스트를 토큰화하고, 토큰을 파싱해서 구문 트리를 만들고, 구문트리를 변환하는 다양한 단계를 거친 다음, 마지막으로 목적 코드를 생성하는 식이다. 각 단계는 자신만의 문제에 집중하기 때문에 나머지 단계에 관해서는 자세히 몰라도 이해할 수 있다.

절차

1. 두 번째 단계에 해당하는 코드를 독립 함수로 추출한다.

2. 테스트한다.

3. 중간 데이터 구조를 만들어서 앞에서 추출한 함수의 인수로 추가한다.

4. 테스트한다.

5. 추출한 두 번째 단계 함수의 매개변수를 하나씩 검토한다. 그중 첫 번째 단계에서 사용되는 것은 중간 데이터 구조로 옮긴다. 하나씩 옮길 때마다 테스트한다.

6. 첫 번째 단계 코드를 함수로 추출하면서 중간 데이터 구조를 반환하도록 만든다

 

function priceOrder(product, quaintity, shippingMethod) {
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshould, 0)
             * product.basePrice * product.discountRate;
    const shippingPerCase = (basePrice > shippingMethod.discountThreshould)
             ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = basePrice - discount + shippingCost;
    return price;
}

간단한 예이지만 계산이 두 단계로 이뤄짐을 알 수 있다. 앞의 몇 줄은 상품 정보를 이용해서 결제 금액 중 상품 가격을 계산한다. 반면 뒤의 코드는 배송 정보를 이용하여 결제 금액 중 배송비를 계산한다. 그래서 두 단계로 나누는 것이 좋다.

 

1. 배송비 계산 부분을 함수로 추출

function priceOrder(product, quaintity, shippingMethod) {
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshould, 0)
             * product.basePrice * product.discountRate;
    const shippingPerCase = (basePrice > shippingMethod.discountThreshould)
             ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = applyShipping(basePrice, shippingMethod, quantity, discount);
    return price;
}

function applyShipping(basePrice, shippingMethod, quantity, discount) {
    const shippingPerCase = (basePrice > shippingMethod.discountThreshould)
            ? shippingMethod.discountFee : shippingMethod.feedPerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = basePrice - discount + shippingCost;
    return price;
}

2. 다음은 첫 번째 단계와 두 번째 단계가 주고받을 중간 데이터 구조를 만든다.

function priceOrder(product, quaintity, shippingMethod) {
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshould, 0)
             * product.basePrice * product.discountRate;
    const priceData = {};
    const price = applyShipping(basePrice, shippingMethod, quantity, discount);
    return price;
}

function applyShipping(priceData, basePrice, shippingMethod, quantity, discount) {
    const shippingPerCase = (basePrice > shippingMethod.discountThreshould)
            ? shippingMethod.discountFee : shippingMethod.feedPerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = basePrice - discount + shippingCost;
    return price;
}

3. applyShipping()에 전달되는 매개변수 중에서 basePrice는 첫 번째 단계를 수행하는 코드에서 생성된다. 따라서 중간 데이터 구조로 옮기고 매개변수 목록에서 제거한다. 또한 shippingMethod에서 quantity와 discount 또한 중간 데이터 구조로 옮긴다.

function priceOrder(product, quaintity, shippingMethod) {
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshould, 0)
             * product.basePrice * product.discountRate;
    const priceData = {basePrice: basePrice, quantity: quantity, discount: discount};
    const price = applyShipping(priceData, shippingMethod, discount);
    return price;
}

function applyShipping(priceData, shippingMethod) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshould)
            ? shippingMethod.discountFee : shippingMethod.feedPerCase;
    const shippingCost = priceData.quantity * shippingPerCase;
    const price = priceData.basePrice - priceData.discount + shippingCost;
    return price;
}

4. 이제 첫 번째 단계 코드를 함수로 호출하고 이 데이터 구조를 반환하게 한다. 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.discountThreshould, 0) *
            product.basPrice * product.discountRate;
    return {basePrice: basePrice, quantity: quantity, discount: discount}
}

function applyShipping(priceData, shippingMethod) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshould)
            ? shippingMethod.discountFee : shippingMethod.feedPerCase;
    const shippingCost = priceData.quantity * shippingPerCase;
    return priceData.basePrice - priceData.discount + shippingCost;
}

 

반응형