IT Knowledge/Development/images/리팩토링-기법-diagram.svg
📌 핵심 개념
쉬운 비유: 방 청소. 물건 위치를 바꾸거나 정리하지만, 물건 자체(기능)는 그대로입니다. 깔끔해져서 찾기 쉬워집니다.
리팩토링 = 동작은 그대로 + 코드 구조 개선
AI 요약 보고서
리팩토링 개념: 기존 동작은 유지하면서 코드 구조를 개선하는 작업으로, 가독성 향상, 재사용성 증대, 테스트 용이성을 목적으로 수행됨. 비유하자면 방 청소와 유사하게, 물건 위치를 정리하는 것임.
주요 기법:
- Extract Function: 긴 함수에서 기능별로 작은 함수로 분리하여 가독성 향상. 예시에서는 주문 처리 과정을
hasEnoughStock,calculateDiscount,calculateTotal,processPayment등으로 분리.- Replace Magic Number: 의미 없는 숫자를 상수로 치환하여 의미 부여. 예를 들어, 배송비와 타이머 시간 등을 명확한 상수(
SHIPPING_COST,FIVE_MINUTES)로 변경.- Remove Dead Code: 사용하지 않는 코드와 주석을 제거하여 코드 정리. 버전 관리 시스템 활용 권장.
- Simplify Conditional: 복잡한 조건문을 조기 반환 또는 규칙 기반 구조로 간소화. 예시에서는 할인율 계산을 명확한 조건문 또는 객체로 재구성.
- Extract Variable: 복잡한 표현식을 의미 있는 변수로 분리하여 가독성 향상. 예를 들어,
subtotal,discountRate,shippingFee등.- Replace Loop with Pipeline: 명령형 루프를 선언적 함수(
filter,map,reduce)로 대체하여 가독성 및 유지보수성 향상.실천 원칙:
- 테스트 우선: 리팩토링 전후 동작 검증을 위한 테스트 작성 후 단계별 개선.
- 작은 단계: 한 번에 전체가 아닌 작은 함수 또는 조건문부터 차근차근 개선.
- 자주 커밋: 변경 사항마다 커밋하여 변경 이력 명확히 유지.
효과: 코드의 명료성, 재사용성, 유지보수성 증대, 버그 방지 가능. 참고 자료로 Martin Fowler의 Refactoring과 Refactoring Guru 추천.
🎯 실전 기법
1. Extract Function (함수 추출)
Before:
// ❌ 하나의 긴 함수 (100줄)
function processOrder(order) {
// 재고 확인
let hasStock = true;
for (const item of order.items) {
const product = db.products.findById(item.productId);
if (product.stock < item.quantity) {
hasStock = false;
break;
}
}
if (!hasStock) {
return { success: false, error: '재고 부족' };
}
// 할인 계산
let discount = 0;
if (order.couponCode) {
const coupon = db.coupons.findByCode(order.couponCode);
if (coupon.type === 'PERCENTAGE') {
discount = order.subtotal * (coupon.value / 100);
} else {
discount = coupon.value;
}
}
// 최종 금액
const total = order.subtotal - discount + order.shippingFee;
// 결제 처리
const payment = paymentGateway.charge(order.userId, total);
// ...더 많은 로직
}After:
// ✅ 작은 함수들로 분리
function processOrder(order) {
if (!hasEnoughStock(order.items)) {
return { success: false, error: '재고 부족' };
}
const discount = calculateDiscount(order);
const total = calculateTotal(order, discount);
const payment = processPayment(order.userId, total);
return { success: true, payment };
}
function hasEnoughStock(items) {
return items.every(item => {
const product = db.products.findById(item.productId);
return product.stock >= item.quantity;
});
}
function calculateDiscount(order) {
if (!order.couponCode) return 0;
const coupon = db.coupons.findByCode(order.couponCode);
return coupon.type === 'PERCENTAGE'
? order.subtotal * (coupon.value / 100)
: coupon.value;
}
function calculateTotal(order, discount) {
return order.subtotal - discount + order.shippingFee;
}
function processPayment(userId, amount) {
return paymentGateway.charge(userId, amount);
}효과:
- 가독성 ⬆️ (한눈에 파악)
- 재사용 가능
- 테스트 쉬움
2. Replace Magic Number (매직 넘버 제거)
Before:
// ❌ 숫자의 의미를 알 수 없음
function calculateShipping(weight) {
if (weight < 5) {
return 3000;
} else if (weight < 20) {
return 5000;
} else {
return 10000;
}
}
setTimeout(() => {
checkStatus();
}, 300000); // 300000이 뭐지?After:
// ✅ 의미 있는 이름
const SHIPPING_COST = {
LIGHT: 3000, // 5kg 미만
MEDIUM: 5000, // 5-20kg
HEAVY: 10000 // 20kg 이상
};
const WEIGHT_THRESHOLD = {
LIGHT: 5,
MEDIUM: 20
};
function calculateShipping(weight) {
if (weight < WEIGHT_THRESHOLD.LIGHT) {
return SHIPPING_COST.LIGHT;
} else if (weight < WEIGHT_THRESHOLD.MEDIUM) {
return SHIPPING_COST.MEDIUM;
} else {
return SHIPPING_COST.HEAVY;
}
}
const FIVE_MINUTES = 5 * 60 * 1000;
setTimeout(() => {
checkStatus();
}, FIVE_MINUTES);3. Remove Dead Code (죽은 코드 제거)
Before:
// ❌ 사용하지 않는 코드
function processUser(user) {
// const oldWay = computeOldWay(user); // 주석 처리된 코드
const result = computeNewWay(user);
// if (DEBUG_MODE) { // 사용 안 하는 조건
// console.log('Debug:', result);
// }
return result;
}
// function computeOldWay(user) { // 더 이상 사용 안 함
// return user.name + user.age;
// }After:
// ✅ 깔끔
function processUser(user) {
return computeNewWay(user);
}
// Git 히스토리에 있으니 필요하면 복원 가능
// 주석 처리하지 말고 삭제!4. Simplify Conditional (조건문 간소화)
Before:
// ❌ 복잡한 조건문
function getDiscount(user, order) {
if (user.isPremium) {
if (order.total > 100000) {
if (user.purchaseCount > 10) {
return 0.3; // 30% 할인
} else {
return 0.2; // 20% 할인
}
} else {
return 0.1; // 10% 할인
}
} else {
if (order.total > 50000) {
return 0.05; // 5% 할인
} else {
return 0; // 할인 없음
}
}
}After:
// ✅ Early Return으로 단순화
function getDiscount(user, order) {
// 일반 회원
if (!user.isPremium) {
return order.total > 50000 ? 0.05 : 0;
}
// 프리미엄 회원
if (order.total <= 100000) {
return 0.1;
}
return user.purchaseCount > 10 ? 0.3 : 0.2;
}
// 또는 객체로
const DISCOUNT_RULES = {
premium: {
vip: 0.3, // 10회 이상 구매 + 10만원 이상
regular: 0.2, // 10만원 이상
basic: 0.1 // 10만원 미만
},
normal: {
high: 0.05, // 5만원 이상
basic: 0 // 5만원 미만
}
};
function getDiscount(user, order) {
if (!user.isPremium) {
return order.total > 50000
? DISCOUNT_RULES.normal.high
: DISCOUNT_RULES.normal.basic;
}
if (order.total <= 100000) {
return DISCOUNT_RULES.premium.basic;
}
return user.purchaseCount > 10
? DISCOUNT_RULES.premium.vip
: DISCOUNT_RULES.premium.regular;
}5. Extract Variable (변수 추출)
Before:
// ❌ 복잡한 표현식
function calculatePrice(order) {
return (
order.items.reduce((sum, item) => sum + item.price * item.quantity, 0) *
(1 - (order.coupon ? order.coupon.discount / 100 : 0)) +
(order.items.reduce((sum, item) => sum + item.weight, 0) > 5 ? 5000 : 3000)
);
}After:
// ✅ 의미 있는 변수로 분리
function calculatePrice(order) {
const subtotal = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const discountRate = order.coupon ? order.coupon.discount / 100 : 0;
const discountedPrice = subtotal * (1 - discountRate);
const totalWeight = order.items.reduce(
(sum, item) => sum + item.weight,
0
);
const shippingFee = totalWeight > 5 ? 5000 : 3000;
return discountedPrice + shippingFee;
}6. Replace Loop with Pipeline (루프를 파이프라인으로)
Before:
// ❌ 명령형 루프
function getActiveUserNames(users) {
const result = [];
for (const user of users) {
if (user.isActive) {
result.push(user.name);
}
}
return result;
}After:
// ✅ 선언적 파이프라인
function getActiveUserNames(users) {
return users
.filter(user => user.isActive)
.map(user => user.name);
}
// 더 복잡한 예시
function getTopProducts(orders) {
return orders
.flatMap(order => order.items) // 모든 주문 아이템
.reduce((acc, item) => {
acc[item.productId] = (acc[item.productId] || 0) + item.quantity;
return acc;
}, {}) // 상품별 총 수량
.entries()
.sort((a, b) => b[1] - a[1]) // 수량순 정렬
.slice(0, 10); // 상위 10개
}💡 리팩토링 원칙
1. 테스트 먼저
// 1. 기존 동작 테스트 작성
test('기존 calculatePrice 동작', () => {
const order = {
items: [{ price: 10000, quantity: 2 }],
coupon: { discount: 10 }
};
expect(calculatePrice(order)).toBe(18000);
});
// 2. 리팩토링
// 3. 테스트가 여전히 통과하는지 확인2. 작은 단계로
1회: 함수 하나 추출
2회: 변수명 개선
3회: 조건문 간소화
...
❌ 한 번에 전체 리팩토링 (위험)
✅ 조금씩 개선 (안전)
3. 커밋 자주
git commit -m "Extract hasEnoughStock function"
git commit -m "Replace magic numbers with constants"
git commit -m "Simplify discount calculation"