본문 바로가기
개발

[리팩터링 2판] JavaScript 리팩터링 도서학습 #3

by 개발과 운동, 그리고 책장 2025. 4. 17.

 

 

해당 내용은 위의 책의 내용을 제가 이해한대로 정리해둔 내용입니다.

이번 포스팅에선 Chapter06 의 내용에 대해 다룹니다. 

 

  • Chapter06 기본적인 리팩터링

 

Chapter06 기본적인 리팩터링

6장에서 소개하는 주요 리팩터링 기법들을 요약하고 예제와 함께 정리했습니다.

 

1.함수 추출하기 (Extract Function)
2.함수 인라인하기 (Inline Function)
3.변수 추출하기 (Extract Variable)
4.변수 인라인하기 (Inline Variable)
5.함수 선언 변경하기 (Change Function Declaration)
6.변수 캡슐화하기 (Encapsulate Variable)
7.변수 이름 바꾸기 (Rename Variable)
8.매개변수 객체 만들기 (Introduce Parameter Object)
9.여러 함수를 클래스로 묶기 (Combine Functions into Class)
10.여러 함수를 변환 함수로 묶기 (Combine Functions into Transform)
11.단계 쪼개기 (Split Phase)

 

1.함수 추출하기 (Extract Function)

 

  • 코드 조각을 함수로 묶고 목적을 잘 드러내는 이름을 붙임
  • 코드의 목적과 구현을 분리하여 이해하기 쉽게 만듦

적용 시점 

  • 코드가 너무 길거나 복잡할 때
  • 동일한 코드가 여러 곳에서 사용될 때
  • 코드 블록의 목적이 구현 방법과 다를 때

리팩터링 전

function printOwing(invoice) {
  let outstanding = 0;
  
  console.log("***********************");
  console.log("**** 고객 채무 ****");
  console.log("***********************");
  
  // 미해결 채무(outstanding)를 계산한다
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
  
  // 마감일을 기록한다
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
  
  // 세부 사항을 출력한다
  console.log(`고객명: ${invoice.customer}`);
  console.log(`채무액: ${outstanding}`);
  console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`);
}

 

리팩터링 후:

 

function printOwing(invoice) {
  printBanner();
  const outstanding = calculateOutstanding(invoice);
  recordDueDate(invoice);
  printDetails(invoice, outstanding);
}

function printBanner() {
  console.log("***********************");
  console.log("**** 고객 채무 ****");
  console.log("***********************");
}

function calculateOutstanding(invoice) {
  let result = 0;
  for (const o of invoice.orders) {
    result += o.amount;
  }
  return result;
}

function recordDueDate(invoice) {
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}

function printDetails(invoice, outstanding) {
  console.log(`고객명: ${invoice.customer}`);
  console.log(`채무액: ${outstanding}`);
  console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`);
}




장점

    • 코드가 더 명확하고 이해하기 쉬워짐
    • 중복을 줄이고 재사용성 증가
    • 함수의 목적이 이름에 드러나 주석이 필요 없어짐
    • 디버깅과 수정이 더 쉬워짐

2.함수 인라인하기 (Inline Function)

  • '함수 추출하기'의 반대 기법
  • 함수 본문이 함수명만큼 명확하거나, 간접 호출이 과하게 많을 때 사용

적용 시점 

  • 함수 본문이 함수명만큼 명확할 때
  • 함수가 너무 짧고 단순해서 별도로 분리할 이점이 없을 때
  • 리팩터링 과정에서 잠시 인라인한 후 다시 추출해야 할 때

리팩터링 전:

function getRating(driver) {
  return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}

function moreThanFiveLateDeliveries(driver) {
  return driver.numberOfLateDeliveries > 5;
}

리팩터링 후:

function getRating(driver) {
  return driver.numberOfLateDeliveries > 5 ? 2 : 1;
}

장점

  • 불필요한 간접 호출을 제거하여 코드를 더 직관적으로 만든다
  • 과도하게 쪼개진 함수들을 정리할 수 있다
  • 더 큰 리팩터링 작업의 중간 단계로 활용할 수 있다

3.변수 추출하기 (Extract Variable)

  • 복잡한 표현식을 더 이해하기 쉽도록 변수로 추출
  • 표현식에 이름을 붙여 의도를 명확히 드러냄

적용 시점 

  • 표현식이 복잡하여 이해하기 어려울 때
  • 동일한 표현식이 여러 번 사용될 때
  • 디버깅 시 중간 값을 확인할 필요가 있을 때

리팩터링 전:

function price(order) {
  // 가격 = 기본 가격 - 수량 할인 + 배송비
  return order.quantity * order.itemPrice -
    Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
    Math.min(order.quantity * order.itemPrice * 0.1, 100);
}

리팩터링 후:

function price(order) {
  // 가격 = 기본 가격 - 수량 할인 + 배송비
  const basePrice = order.quantity * order.itemPrice;
  const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
  const shipping = Math.min(basePrice * 0.1, 100);
  
  return basePrice - quantityDiscount + shipping;
}

 

장점

  • 복잡한 로직을 이해하기 쉽게 분해한다
  • 코드의 의도가 더 명확해진다
  • 중복 계산을 줄일 수 있다
  • 디버깅이 더 쉬워진다

4.변수 인라인하기 (Inline Variable)

  • '변수 추출하기'의 반대 기법
  • 변수가 원래 표현식과 다를 바 없을 때 직접 표현식을 사용

적용 시점 

  • 변수명이 원래 표현식보다 더 명확하지 않을 때
  • 변수가 리팩터링을 방해할 때
  • 다른 리팩터링의 중간 단계로 필요할 때

리팩터링 전:

function isDeliveryFree(anOrder) {
  let basePrice = anOrder.basePrice;
  return basePrice > 1000;
}

 

리팩터링 후:

function isDeliveryFree(anOrder) {
  return anOrder.basePrice > 1000;
}

 

장점

  • 불필요한 간접 참조를 제거하여 코드를 더 직관적으로 만든다
  • 더 큰 리팩터링의 중간 단계로 활용될 수 있다

5.함수 선언 변경하기 (Change Function Declaration)

  • 함수의 이름이나 매개변수를 변경하여 더 명확하게 만듦
  • 일관된 함수 인터페이스를 유지하기 위해 사용

적용 시점 

  • 함수명이 함수의 목적을 명확히 드러내지 못할 때
  • 매개변수 목록이 일관되지 않거나 혼란스러울 때
  • 매개변수를 추가하거나 제거해야 할 때

리팩터링 전:

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

리팩터링 후:

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

마이그레이션 절차 예시:

// 1단계: 새 함수 생성
function circumference(radius) {
  return 2 * Math.PI * radius;
}

// 2단계: 원래 함수를 위임 함수로 변경
function circum(radius) {
  return circumference(radius);
}

// 3단계: 호출문을 하나씩 새 함수 사용으로 변경
// 원래 호출: const result = circum(5);
// 새 호출: const result = circumference(5);

// 4단계: 모든 호출문이 변경되면 위임 함수 제거
// function circum(radius) { ... } 삭제

장점

 

  • 함수의 이름이 목적을 더 명확히 표현한다
  • 함수 인터페이스가 일관되고 예측 가능해진다
  • API 설계가 개선된다

6.변수 캡슐화하기 (Encapsulate Variable)

  • 데이터를 직접 접근하지 않고 함수를 통해 접근하도록 변경
  • 데이터 구조 변경과 사용 로직을 분리하여 유지보수성 향상

적용 시점 

  • 데이터가 여러 곳에서 참조될 때
  • 데이터 구조가 변경될 가능성이 있을 때
  • 데이터 접근에 추가 로직이 필요할 때(검증, 로깅 등)

리팩터링 전:

let defaultOwner = { firstName: "마틴", lastName: "파울러" };

// 다른 파일에서
spaceship.owner = defaultOwner;
defaultOwner = { firstName: "레베카", lastName: "파슨스" };

리팩터링 후:

// defaultOwner.js 파일
let defaultOwnerData = { firstName: "마틴", lastName: "파울러" };

export function defaultOwner() { return Object.assign({}, defaultOwnerData); }
export function setDefaultOwner(arg) { defaultOwnerData = arg; }

// 다른 파일에서
import { defaultOwner, setDefaultOwner } from "./defaultOwner";

spaceship.owner = defaultOwner();
setDefaultOwner({ firstName: "레베카", lastName: "파슨스" });

장점

 

  • 데이터 변경 지점을 한 곳으로 모아 관리가 용이해진다
  • 데이터 접근 시 추가 로직(유효성 검사, 로깅 등)을 쉽게 추가할 수 있다
  • 데이터 구조 변경 시 영향 범위를 최소화할 수 있다
  • 불변성(immutability)을 쉽게 구현할 수 있다

7.변수 이름 바꾸기 (Rename Variable)

  • 변수명을 더 명확하고 의미 있게 변경
  • 코드의 가독성과 이해도 향상

적용 시점 

  • 변수명이 목적이나 의도를 명확히 드러내지 못할 때
  • 변수의 역할이 변경되었을 때
  • 코드 컨벤션에 맞추기 위해

리팩터링 전:

let a = height * width;
return a;

리팩터링 후:

let area = height * width;
return area;

넓은 범위의 경우:

// 리팩터링 전
let tpHd = "untitled";

// 중간 단계: 캡슐화
function getTitle() { return tpHd; }
function setTitle(arg) { tpHd = arg; }

// 리팩터링 후
let title = "untitled";

function getTitle() { return title; }
function setTitle(arg) { title = arg; }

장점

 

  • 코드의 가독성과 이해도가 향상된다
  • 변수의 목적이 명확해져 유지보수가 쉬워진다
  • 코드 문서화 효과가 있다

8.매개변수 객체 만들기 (Introduce Parameter Object)

개념

  • 여러 개의 매개변수를 하나의 객체로 묶음
  • 데이터 구조와 관련 동작을 응집력 있게 관리

적용 시점

  • 여러 함수가 같은 매개변수 그룹을 사용할 때
  • 데이터 항목 사이에 논리적 연관성이 있을 때
  • 매개변수 목록이 너무 길 때

리팩터링 전:

function amountInvoiced(startDate, endDate) { /* ... */ }
function amountReceived(startDate, endDate) { /* ... */ }
function amountOverdue(startDate, endDate) { /* ... */ }

리팩터링 후:

// 매개변수 객체 정의
class DateRange {
  constructor(startDate, endDate) {
    this._startDate = startDate;
    this._endDate = endDate;
  }
  
  get startDate() { return this._startDate; }
  get endDate() { return this._endDate; }
}

// 함수에서 매개변수 객체 사용
function amountInvoiced(dateRange) { /* ... */ }
function amountReceived(dateRange) { /* ... */ }
function amountOverdue(dateRange) { /* ... */ }

// 호출 시
const range = new DateRange(start, end);
const amount = amountInvoiced(range);

장점

 

  • 매개변수 간의 관계가 명확해진다
  • 매개변수 목록이 간결해진다
  • 관련된 동작을 데이터 구조에 추가할 수 있다
  • 코드 일관성이 향상된다

9.여러 함수를 클래스로 묶기 (Combine Functions into Class)

  • 같은 데이터를 사용하는 여러 함수를 클래스로 묶음
  • 객체 지향적 접근으로 코드 구조화

적용 시점 

  • 여러 함수가 같은 데이터를 다룰 때
  • 함수들 사이에 명확한 관계가 있을 때
  • 데이터와 함수를 더 응집력 있게 관리하고 싶을 때

리팩터링 전:

function base(reading) { return reading.base; }
function taxableCharge(reading) {
  return Math.max(0, base(reading) - taxThreshold(reading));
}
function taxThreshold(reading) { return reading.year < 2019 ? 0.1 : 0.2; }

리팩터링 후:

class Reading {
  constructor(data) {
    this._customer = data.customer;
    this._quantity = data.quantity;
    this._month = data.month;
    this._year = data.year;
  }
  
  get base() { return this._quantity * this.baseRate; }
  
  get taxableCharge() {
    return Math.max(0, this.base - this.taxThreshold);
  }
  
  get taxThreshold() {
    return this._year < 2019 ? 0.1 : 0.2;
  }
  
  get baseRate() {
    // 기본 요금 계산 로직
  }
}

// 사용 예
const rawReading = { customer: "ivan", quantity: 10, month: 5, year: 2017 };
const reading = new Reading(rawReading);
const baseCharge = reading.base;
const taxableCharge = reading.taxableCharge;

장점

 

  • 관련 데이터와 함수가 응집력 있게 묶인다
  • 클래스 인터페이스를 통해 데이터 접근을 제어할 수 있다
  • 코드의 의도가 더 명확해진다
  • 확장성이 향상된다

10.여러 함수를 변환 함수로 묶기 (Combine Functions into Transform)

  • 원본 데이터는 그대로 두고 필요한 정보를 도출하는 변환 함수 사용
  • 입력 데이터로부터 파생 데이터를 만들어내는 구조로 변경

적용 시점 

  • 원본 데이터를 변경하지 않고 파생 데이터를 만들어야 할 때
  • 여러 함수가 같은 입력 데이터에서 다양한 값을 도출할 때
  • 불변성(immutability)을 유지하고 싶을 때

리팩터링 전:

function base(reading) { return reading.base; }
function taxableCharge(reading) {
  return Math.max(0, base(reading) - taxThreshold(reading));
}
function taxThreshold(reading) { return reading.year < 2019 ? 0.1 : 0.2; }

리팩터링 후:

function enrichReading(original) {
  const result = Object.assign({}, original);
  result.baseCharge = calculateBaseCharge(result);
  result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result));
  return result;
}

function calculateBaseCharge(reading) {
  return reading.quantity * baseRate(reading.month, reading.year);
}

function taxThreshold(reading) {
  return reading.year < 2019 ? 0.1 : 0.2;
}

// 사용 예
const rawReading = { customer: "ivan", quantity: 10, month: 5, year: 2017 };
const enrichedReading = enrichReading(rawReading);
const baseCharge = enrichedReading.baseCharge;
const taxableCharge = enrichedReading.taxableCharge;

장점

 

  • 원본 데이터의 불변성을 유지할 수 있다
  • 파생 데이터를 일관되게 관리할 수 있다
  • 계산 로직이 한 곳에 모여 중복을 방지한다
  • 함수형 프로그래밍 스타일과 잘 어울린다

11.단계 쪼개기 (Split Phase)

  • 서로 다른 두 대의 작업을 별도의 모듈로 분리
  • 논리적으로 구분되는 단계를 명확히 분리하여 코드 이해도 향상

적용 시점 

  • 하나의 함수가 여러 다른 작업을 수행할 때
  • 코드의 두 부분이 서로 다른 데이터와 논리를 다룰 때
  • 처리 과정에 뚜렷한 단계가 있을 때

리팩터링 전:

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 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, quantity, 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;
}

장점

 

  • 각 단계의 목적이 명확해진다
  • 각 단계를 독립적으로 테스트하고 디버깅할 수 있다
  • 코드 재사용성이 향상된다
  • 변경 사항이 한 단계에만 영향을 미치도록 격리할 수 있다

 


마무리

  1. 코드의 의도를 명확히 하라 - 함수와 변수 이름은 목적을 분명히 드러내야 합니다.
  2. 작은 단위로 리팩터링하고 테스트하라 - 큰 변경보다는 작은 단계로 나누어 안전하게 진행합니다.
  3. 중복을 제거하라 - 같은 코드가 여러 곳에 있다면 추출하여 재사용합니다.
  4. 코드를 적절한 위치에 배치하라 - 관련 있는 데이터와 함수는 함께 두어 응집도를 높입니다.
  5. 데이터는 적절히 캡슐화하라 - 데이터 변경 지점을 최소화하여 관리를 용이하게 합니다.

 

리팩터링 2장의 챕터 6을 학습하며 주요 개념들을 정리했습니다. 실제 개발 환경에서는 더 복잡한 데이터 구조를 다루며 이러한 개념들을 적용해야 하겠지만, 리팩터링의 목적과 장점을 명확히 설명하기 위해서는 이론적 기반을 탄탄히 다지는 것이 중요하다고 생각합니다. 앞으로 제 코드베이스도 이러한 원칙들을 적용하여 꾸준히 개선해 나가고 싶습니다.

 

댓글