해당 내용은 위의 책의 내용을 제가 이해한대로 정리해둔 내용입니다.
이번 포스팅에선 Chapter06 의 내용에 대해 다룹니다.
- Chapter06 기본적인 리팩터링
Chapter06 기본적인 리팩터링
6장에서 소개하는 주요 리팩터링 기법들을 요약하고 예제와 함께 정리했습니다.
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;
}
장점
- 각 단계의 목적이 명확해진다
- 각 단계를 독립적으로 테스트하고 디버깅할 수 있다
- 코드 재사용성이 향상된다
- 변경 사항이 한 단계에만 영향을 미치도록 격리할 수 있다
마무리
- 코드의 의도를 명확히 하라 - 함수와 변수 이름은 목적을 분명히 드러내야 합니다.
- 작은 단위로 리팩터링하고 테스트하라 - 큰 변경보다는 작은 단계로 나누어 안전하게 진행합니다.
- 중복을 제거하라 - 같은 코드가 여러 곳에 있다면 추출하여 재사용합니다.
- 코드를 적절한 위치에 배치하라 - 관련 있는 데이터와 함수는 함께 두어 응집도를 높입니다.
- 데이터는 적절히 캡슐화하라 - 데이터 변경 지점을 최소화하여 관리를 용이하게 합니다.
리팩터링 2장의 챕터 6을 학습하며 주요 개념들을 정리했습니다. 실제 개발 환경에서는 더 복잡한 데이터 구조를 다루며 이러한 개념들을 적용해야 하겠지만, 리팩터링의 목적과 장점을 명확히 설명하기 위해서는 이론적 기반을 탄탄히 다지는 것이 중요하다고 생각합니다. 앞으로 제 코드베이스도 이러한 원칙들을 적용하여 꾸준히 개선해 나가고 싶습니다.
'개발' 카테고리의 다른 글
[리팩터링 2판] JavaScript 리팩터링 도서학습 #4 (4) | 2025.04.24 |
---|---|
[리팩터링 2판] JavaScript 리팩터링 도서학습 #2 (4) | 2025.03.29 |
[리팩터링 2판] JavaScript 리팩터링 도서학습 #1 (5) | 2025.03.23 |
모듈 페더레이션(Module Federation) 이해하기 (6) | 2025.01.26 |
댓글