목차
3. Long Function(긴 함수)
긴 함수가 문제인 이유는 단순히 줄 수가 많아서가 아니다. 서로 다른 판단을 한곳에서 해야 하거나, 중간 상태를 머릿속에 오래 붙잡아야 할 때 변경이 어려워진다. 반대로 한 알고리즘의 연속된 단계가 짧고 명확하다면 억지로 여러 함수로 쪼개지 않아도 된다. 함수를 읽을 때 “이 묶음은 왜 필요한가?”라는 질문에 코드만으로 답하기 어려운 부분을 찾는다.
주문 총액 예제로 추출 지점 찾기
다음 함수는 소계, 할인, 배송비를 한 번에 계산한다. discountRate는 0 이상 1 이하의 비율이라고 가정한다.
function totalWon(
lines: { unitPriceWon: number; quantity: number }[],
discountRate: number,
express: boolean,
): number {
let subtotal = 0;
for (const line of lines) {
subtotal += line.unitPriceWon * line.quantity;
}
const discounted = Math.round(subtotal * (1 - discountRate));
const deliveryFee = express ? 5000 : subtotal >= 30000 ? 0 : 3000;
return discounted + deliveryFee;
}
예를 들어 1만 원짜리 2개, 할인율 10%, 일반 배송이면 소계 2만 원, 할인 후 1만 8천 원, 배송비 3천 원으로 총 2만 1천 원이다. 배송비 무료 기준이 할인 전 소계에 적용된다는 점이 코드에 숨어 있다. 리팩터링 전에 이 정책이 맞는지 업무 담당자와 확인하고 테스트로 고정해야 한다.
type Line = { unitPriceWon: number; quantity: number };
type Delivery = "standard" | "express";
function subtotalWon(lines: Line[]): number {
return lines.reduce((sum, line) => sum + line.unitPriceWon * line.quantity, 0);
}
function deliveryFeeWon(subtotal: number, delivery: Delivery): number {
if (delivery === "express") return 5000;
return subtotal >= 30000 ? 0 : 3000;
}
function totalWon(lines: Line[], discountRate: number, delivery: Delivery): number {
if (discountRate < 0 || discountRate > 1) {
throw new RangeError("할인율은 0 이상 1 이하여야 합니다");
}
const subtotal = subtotalWon(lines);
const discounted = Math.round(subtotal * (1 - discountRate));
return discounted + deliveryFeeWon(subtotal, delivery);
}
subtotalWon은 항목 합산이라는 의도를 이름으로 드러내고, deliveryFeeWon은 배송 정책의 변경 지점을 분리한다. 함수 추출과 별도로 express 불리언을 명시적인 배송 유형으로 바꾸고 할인율 검사를 추가했다. 순수한 리팩터링만 수행할 때는 이 두 동작 변경을 별도 단계로 나눠야 한다. 그래야 테스트 실패의 원인을 구분할 수 있다.
함수 추출이 어려울 때
추출한 함수에 지역 변수 8개를 매개변수로 넘겨야 한다면, 관련 값이 하나의 개념인지 먼저 본다. startDate, endDate, timezone이 여러 함수에 함께 등장하면 기간 값 객체를 만드는 Introduce Parameter Object가 도움이 된다. 한 객체에서 customer.id와 customer.grade를 꺼내 여러 인자로 넘기는 경우에는 Preserve Whole Object를 검토한다. 다만 필요한 값이 두 개뿐인 함수가 거대한 객체 전체에 의존하게 된다면 오히려 결합도가 높아질 수 있다.
Replace Temp with Query는 중간 계산을 이름 있는 질의로 옮긴다. 계산이 순수하고 저렴할 때 효과적이지만, 데이터베이스 조회나 큰 반복 계산을 여러 번 호출하게 만들면 성능과 동작이 달라질 수 있다. Slide Statements로 관련 코드를 가까이 모은 뒤 추출하면 경계가 보이기 쉽다. if의 조건과 결과가 복잡하면 Decompose Conditional, 여러 곳의 같은 종류별 분기가 함께 바뀐다면 Replace Conditional with Polymorphism을 검토한다.
예제에서 빈 항목 목록의 소계는 0이다. 할인율 경계값 0과 1, 무료 배송 경계 29,999원과 30,000원, 특급 배송을 확인하면 추출 과정에서 정책이 바뀌었는지 알 수 있다. 금액을 부동소수점으로 다루는 정책은 이 예제의 범위 밖이므로 실제 결제 시스템에서는 정수 최소 화폐 단위 또는 정밀한 금액 타입을 사용한다.