본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로소프트웨어 공학 › 코드 품질 › 리팩터링

3. Long Function(긴 함수)

목차

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원, 특급 배송을 확인하면 추출 과정에서 정책이 바뀌었는지 알 수 있다. 금액을 부동소수점으로 다루는 정책은 이 예제의 범위 밖이므로 실제 결제 시스템에서는 정수 최소 화폐 단위 또는 정밀한 금액 타입을 사용한다.

참고: Refactoring 공식 카탈로그

같은 카테고리의 글