함수는 하나의 일만 하는게 좋다는 건 여러 곳에서 들어봤을 것이다.
특히나 handleButtonClick
과 같은 이름을 짓게 되는 경우가 많다. 이런 이름은 간단한 동작의 경우에는 적절한 네이밍이지만, 막상 잘 뜯어보면 여러 일을 동시에 하고 있는 경우가 많다.
그래서 잘 뜯어보면 함수들을 여러 개로 분리하는게 구조 파악 + 확장을 하기에 용이하다.
함수 단계 쪼개기
서로 다른 두 대상을 한꺼번에 다루는 코드를 발견하면 각각을 별개모듈로 나누는 방법이다.
예를 들어 입력값이 처리 로직에 적합하지 않은 형태로 들어오는 경우, 본 작업에 들어가기 전에 입력값을 다루기 편한 형태로 가공한다. 혹은 로직을 순차적인 단계들로 분리하거나 해도 된다.
단계를 쪼개는 것은 보통 큰 소프트웨어에 많이 한다. 하지만 규모와 상관없이 분리하기가 좋은 코드들은 단계쪼개기를 하는 것이 좋다.
다른 단계로 볼 수있는 코드 영역들이 마침 서로 다른 데이터와 함수를 사용한다면 → 단계쪼개기에 적합하다
상품의 결제금액을 계산하는 코드 단계 쪼개기
function priceOrder(product, quantity, shippingMethod) {
const basePrice = product.basePrice * quantity;
const discount =
Math.max(quantity - product.discountThreshold, 0) *
product.basePrice *
product.discountRate;
// 여기 위까지는 상품정보를 이용해서 상품가격을 계산한다.
const shippengPerCase =
basePrice > shippingMethod.discountThreshold
? shippingMethod.discountFee
: shippingMethod.feePerCase;
const shippingCost = quantity * shippengPerCase;
// 여기 위까지는 배송정보를 이용하여 결제금액 중 배송비를 계산한다.
const price = basePrice - discount + shippingCost;
return price;
}
위 priceOrder
함수는 계산이 두 단계로 이루어진다.
- 결제 금액 중 상품 가격을 계산한다.
- 배송정보를 이용하여 결제금액 중 배송비를 계산한다.
나중에 상품가격과 배송비 계산이 더 복잡해지는 변경이 생길 수 있으므로 두 단계로 나누는 것이 좋다. 그래야 나중에 수정할때 한 문제에만 집중할 수 있다. 리팩토링 할때에는 눈에 확연히 보인다고 하더라도 절차를 따라가는 것이 좋다.
단계 1. 배송비 계산을 함수로 추출한다.
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 shippengPerCase =
basePrice > shippingMethod.discountThreshold
? shippingMethod.discountFee
: shippingMethod.feePerCase;
const shippingCost = quantity * shippengPerCase;
const price = basePrice - discount + shippingCost;
return price;
}
두 번째에 필요한 데이터를 모두 개별 매개변수로 전달한다. 어 이런 것보다 {basePrice, shippingMethod, quantity, discount}
과 같이 객체로 전달하는게 더 좋지 않나요? 라는 물음을 할 수 있는데
이 매개변수들은 다 걸러져서 없어질 것이기 때문에 일단 이렇게 작성한다.
단계 2. 첫 번째 단계와 두 번째 단계가 주고받을 중간 데이터 구조를 만든다.
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 shippengPerCase =
basePrice > shippingMethod.discountThreshold
? shippingMethod.discountFee
: shippingMethod.feePerCase;
const shippingCost = quantity * shippengPerCase;
const price = basePrice - discount + shippingCost;
return price;
}
단계 3. 매개 변수들을 하나씩 옮기면서 처리해준다.
일단 basePrice는 첫 번째 기본 가격을 계산할 때 생겨나므로 중간 데이터 구조에 넣는다.
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 };// 중간 구조에 기본 가격을 넣는다.
const price = applyShipping(priceData, shippingMethod, quantity, discount);
return price;
}
function applyShipping(priceData, shippingMethod, quantity, discount) { // basePrice는 없앤다.
const shippengPerCase =
priceData.basePrice > shippingMethod.discountThreshold
? shippingMethod.discountFee
: shippingMethod.feePerCase;
const shippingCost = quantity * shippengPerCase;
const price = priceData.basePrice - discount + shippingCost;
return price;
}
shippingMethod
는 첫번째 단계에서는 없으니 그대로 두지만, discount,quantity
는 옮기는 것이 깔끔하다.
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, quantity, discount };
const price = applyShipping(priceData, shippingMethod);
return price;
}
function applyShipping(priceData, shippingMethod) {
const shippengPerCase =
priceData.basePrice > shippingMethod.discountThreshold
? shippingMethod.discountFee
: shippingMethod.feePerCase;
const shippingCost = priceData.quantity * shippengPerCase;
const price = priceData.basePrice - priceData.discount + shippingCost;
return price;
}
단계 4. 첫번째 단계를 함수로 추출하고 데이터 구조를 반환하게 한다.
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;
return { basePrice, quantity, discount };
}
function applyShipping(priceData, shippingMethod) {
const shippengPerCase =
priceData.basePrice > shippingMethod.discountThreshold
? shippingMethod.discountFee
: shippingMethod.feePerCase;
const shippingCost = priceData.quantity * shippengPerCase;
const price = priceData.basePrice - priceData.discount + shippingCost;
return price;
}
위를 가격을 계산하는 로직을 변경해야 한다고 할때, 어떤 함수를 고쳐야 하는지, 이때 다른 로직에 영향이 가지 않을지를 걱정하지 않아도 된다.
사실 단계 쪼개기는 이미 알게 모르게 많은 사람들이 사용하고 있었을 것이다. 다만, 이렇게 중간 데이터 구조를 만들지 않고 일단 만들었던 기억이 있다.
함수가 작을 경우에는 상관없지만, 정석적으로 처리하는 방법은 위와 같이 객체 형식으로 중간데이터 구조를 만들고 하나씩 테스트코드를 돌려가며 리팩토링하는 것이 맞다.