안녕하세요, 성장하는 개발자 여러분!
“이 코드 누가 짰어?” 😱
실무에서 가장 많이 듣는 말 중 하나입니다. 하지만 놀랍게도 6개월 전 내가 짠 코드일 가능성이 높죠! 코드는 살아있는 생물처럼 계속 성장하고 변화해야 합니다. 오늘은 제가 실무에서 겪은 다양한 상황들을 통해 배운 리팩터링의 모든 것을 정리해 드리겠습니다.
“리팩터링은 위험해”라고 생각하셨다면, 이 글을 읽고 나면 “리팩터링은 필수야”로 생각이 바뀔 거예요! 🚀
1. 리팩터링이란? 코드의 성형수술이 아닌 건강검진
리팩터링의 정확한 정의
// ❌ 이건 리팩터링이 아닙니다
function oldFunction() {
// 기존 기능을 완전히 바꿔버림
return "completely different behavior";
}
function newFunction() {
// 새로운 기능 추가
return "new feature added";
}
// ✅ 이것이 진짜 리팩터링입니다
// BEFORE: 복잡하고 이해하기 어려운 코드
function calculatePrice(items, userType, discount) {
let total = 0;
for (let i = 0; i < items.length; i++) {
if (userType === "premium") {
total += items[i].price * 0.9;
} else if (userType === "vip") {
total += items[i].price * 0.8;
} else {
total += items[i].price;
}
}
if (discount) {
total = total * (1 - discount);
}
return total;
}
// AFTER: 같은 동작, 더 명확한 구조
function calculatePrice(items, userType, discount = 0) {
const baseTotal = calculateBaseTotal(items);
const discountedTotal = applyUserDiscount(baseTotal, userType);
return applyAdditionalDiscount(discountedTotal, discount);
}
function calculateBaseTotal(items) {
return items.reduce((total, item) => total + item.price, 0);
}
function applyUserDiscount(total, userType) {
const discountRates = {
premium: 0.9,
vip: 0.8,
regular: 1.0,
};
return total * (discountRates[userType] || 1.0);
}
function applyAdditionalDiscount(total, discount) {
return total * (1 - discount);
}
리팩터링의 핵심 원칙
- 겉보기 동작은 그대로 유지: 외부에서 보는 결과는 동일해야 함
- 내부 구조만 개선: 가독성, 유지보수성, 확장성 향상
- 작은 단계로 진행: 한 번에 하나씩, 안전하게
- 테스트 주도: 리팩터링 전후 테스트로 검증
2. 왜 리팩터링이 필요할까? 현실적인 이유들
코드 부채(Technical Debt)의 누적
# 📈 코드 부채 누적 과정
Week 1: "빨리 만들어야 해!"
→ 급하게 작성한 코드
Week 4: "버그 수정만 빠르게!"
→ 임시방편 코드 추가
Week 8: "새 기능 추가해야 해!"
→ 기존 코드에 억지로 끼워 넣기
Week 12: "이 코드를 아무도 건드리고 싶어하지 않아요"
→ 개발 속도 급감, 버그 증가
# 이때 필요한 것이 리팩터링!
실무에서 겪는 문제들
// 😱 실제로 보게 되는 코드들
// 1. 하나의 함수가 너무 많은 일을 함
function processUser(userData) {
// 100줄 넘는 함수
// - 유효성 검사
// - 데이터 변환
// - DB 저장
// - 이메일 발송
// - 로그 기록
// - 캐시 업데이트
// - 알림 발송
}
// 2. 매직 넘버와 하드코딩의 남발
if (user.age > 18 && user.score > 750 && user.level === 3) {
// 이 숫자들이 뭘 의미하는지 아무도 모름
}
// 3. 깊게 중첩된 조건문
if (user) {
if (user.isActive) {
if (user.hasPermission) {
if (user.subscription) {
if (user.subscription.isValid) {
// 실제 로직은 여기 한 줄
}
}
}
}
}
// 4. 의미 없는 변수명
function calc(a, b, c) {
const x = a * b;
const y = x + c;
const z = y * 0.1;
return z;
}
3. 리팩터링 전 준비: 안전망 구축하기
1단계: 테스트 코드 작성
// 리팩터링 전 반드시 테스트 코드부터!
describe("calculatePrice 함수", () => {
test("일반 사용자의 가격 계산", () => {
const items = [{ price: 1000 }, { price: 2000 }];
const result = calculatePrice(items, "regular");
expect(result).toBe(3000);
});
test("프리미엄 사용자 10% 할인", () => {
const items = [{ price: 1000 }];
const result = calculatePrice(items, "premium");
expect(result).toBe(900);
});
test("VIP 사용자 20% 할인", () => {
const items = [{ price: 1000 }];
const result = calculatePrice(items, "vip");
expect(result).toBe(800);
});
test("추가 할인 적용", () => {
const items = [{ price: 1000 }];
const result = calculatePrice(items, "regular", 0.1);
expect(result).toBe(900);
});
test("빈 배열 처리", () => {
const result = calculatePrice([], "regular");
expect(result).toBe(0);
});
});
2단계: 버전 관리 체계 구축
# Git을 활용한 안전한 리팩터링
git checkout -b refactor/improve-price-calculation
# 작은 단위로 커밋
git commit -m "refactor: extract calculateBaseTotal function"
git commit -m "refactor: extract applyUserDiscount function"
git commit -m "refactor: extract applyAdditionalDiscount function"
git commit -m "refactor: improve variable naming in price calculation"
# 각 커밋마다 테스트 실행
npm test
4. 핵심 리팩터링 패턴들
패턴 1: 함수 추출하기 (Extract Function)
// 🔧 가장 자주 사용하는 리팩터링 기법
// BEFORE: 하나의 함수가 너무 많은 일을 함
function processOrder(orderData) {
// 주문 검증
if (!orderData.items || orderData.items.length === 0) {
throw new Error("주문 항목이 없습니다");
}
if (!orderData.customerId) {
throw new Error("고객 ID가 필요합니다");
}
// 가격 계산
let total = 0;
for (const item of orderData.items) {
total += item.price * item.quantity;
}
// 할인 적용
if (orderData.coupon) {
if (orderData.coupon.type === "percent") {
total = total * (1 - orderData.coupon.value / 100);
} else if (orderData.coupon.type === "fixed") {
total = total - orderData.coupon.value;
}
}
// 세금 계산
const tax = total * 0.1;
const finalTotal = total + tax;
// DB 저장
const order = {
id: generateOrderId(),
customerId: orderData.customerId,
items: orderData.items,
subtotal: total,
tax: tax,
total: finalTotal,
status: "pending",
createdAt: new Date(),
};
// 실제 저장 로직...
return order;
}
// AFTER: 책임별로 함수 분리
function processOrder(orderData) {
validateOrder(orderData);
const subtotal = calculateSubtotal(orderData.items);
const discountedTotal = applyCoupon(subtotal, orderData.coupon);
const tax = calculateTax(discountedTotal);
const finalTotal = discountedTotal + tax;
return createOrder({
customerId: orderData.customerId,
items: orderData.items,
subtotal: discountedTotal,
tax,
total: finalTotal,
});
}
function validateOrder(orderData) {
if (!orderData.items?.length) {
throw new Error("주문 항목이 없습니다");
}
if (!orderData.customerId) {
throw new Error("고객 ID가 필요합니다");
}
}
function calculateSubtotal(items) {
return items.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
}
function applyCoupon(total, coupon) {
if (!coupon) return total;
switch (coupon.type) {
case "percent":
return total * (1 - coupon.value / 100);
case "fixed":
return Math.max(0, total - coupon.value);
default:
return total;
}
}
function calculateTax(amount) {
return amount * 0.1;
}
function createOrder({ customerId, items, subtotal, tax, total }) {
return {
id: generateOrderId(),
customerId,
items,
subtotal,
tax,
total,
status: "pending",
createdAt: new Date(),
};
}
패턴 2: 변수 추출하기 (Extract Variable)
// 🔧 복잡한 표현식을 의미 있는 변수로 분리
// BEFORE: 의미를 파악하기 어려운 코드
function calculateShippingCost(order) {
return order.weight > 10 && order.destination === "international" ? order.basePrice * 0.15 + 25 : order.basePrice * 0.05;
}
// AFTER: 의도가 명확한 코드
function calculateShippingCost(order) {
const isHeavyPackage = order.weight > 10;
const isInternationalShipping = order.destination === "international";
const requiresSpecialHandling = isHeavyPackage && isInternationalShipping;
const internationalRate = 0.15;
const domesticRate = 0.05;
const specialHandlingFee = 25;
if (requiresSpecialHandling) {
return order.basePrice * internationalRate + specialHandlingFee;
} else {
return order.basePrice * domesticRate;
}
}
패턴 3: 조건부 로직 개선
// 🔧 복잡한 조건문을 객체나 함수로 대체
// BEFORE: 끝없이 늘어나는 if-else
function getDiscountRate(user) {
if (user.type === "premium" && user.yearsActive >= 5) {
return 0.2;
} else if (user.type === "premium" && user.yearsActive >= 2) {
return 0.15;
} else if (user.type === "premium") {
return 0.1;
} else if (user.type === "regular" && user.yearsActive >= 3) {
return 0.05;
} else {
return 0;
}
}
// AFTER: 객체를 활용한 깔끔한 로직
const DISCOUNT_RULES = {
premium: {
5: 0.2, // 5년 이상
2: 0.15, // 2년 이상
0: 0.1, // 기본
},
regular: {
3: 0.05, // 3년 이상
0: 0, // 기본
},
};
function getDiscountRate(user) {
const userRules = DISCOUNT_RULES[user.type] || DISCOUNT_RULES.regular;
// 조건을 만족하는 가장 높은 할인율 찾기
const eligibleDiscounts = Object.entries(userRules)
.filter(([years]) => user.yearsActive >= parseInt(years))
.map(([_, rate]) => rate);
return Math.max(...eligibleDiscounts, 0);
}
// 또는 더 명확한 함수형 접근
function getDiscountRate(user) {
if (user.type === "premium") {
return getPremiumDiscount(user.yearsActive);
}
if (user.type === "regular") {
return getRegularDiscount(user.yearsActive);
}
return 0;
}
function getPremiumDiscount(yearsActive) {
if (yearsActive >= 5) return 0.2;
if (yearsActive >= 2) return 0.15;
return 0.1;
}
function getRegularDiscount(yearsActive) {
return yearsActive >= 3 ? 0.05 : 0;
}
패턴 4: 매직 넘버/문자열 제거
// 🔧 하드코딩된 값들을 상수로 분리
// BEFORE: 매직 넘버들의 향연
function validateUser(user) {
if (user.age < 18) return false;
if (user.score < 750) return false;
if (user.level !== 3 && user.level !== 4 && user.level !== 5) return false;
if (user.status !== "active" && user.status !== "premium") return false;
return true;
}
// AFTER: 의미가 명확한 상수들
const USER_VALIDATION = {
MIN_AGE: 18,
MIN_SCORE: 750,
VALID_LEVELS: [3, 4, 5],
VALID_STATUSES: ["active", "premium"],
};
function validateUser(user) {
return isAgeValid(user.age) && isScoreValid(user.score) && isLevelValid(user.level) && isStatusValid(user.status);
}
function isAgeValid(age) {
return age >= USER_VALIDATION.MIN_AGE;
}
function isScoreValid(score) {
return score >= USER_VALIDATION.MIN_SCORE;
}
function isLevelValid(level) {
return USER_VALIDATION.VALID_LEVELS.includes(level);
}
function isStatusValid(status) {
return USER_VALIDATION.VALID_STATUSES.includes(status);
}
패턴 5: 중복 코드 제거 (DRY 원칙)
// 🔧 반복되는 코드를 공통 함수로 추출
// BEFORE: 비슷한 코드가 여러 곳에 중복
function saveUser(userData) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] Saving user: ${userData.id}`);
if (!userData.email) {
throw new Error("Email is required");
}
// DB 저장 로직
const result = database.save("users", userData);
console.log(`[${timestamp}] User saved successfully: ${userData.id}`);
return result;
}
function saveProduct(productData) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] Saving product: ${productData.id}`);
if (!productData.name) {
throw new Error("Name is required");
}
// DB 저장 로직
const result = database.save("products", productData);
console.log(`[${timestamp}] Product saved successfully: ${productData.id}`);
return result;
}
// AFTER: 공통 로직을 추출
function saveEntity(entityType, data, requiredFields) {
logOperation("start", entityType, data.id);
validateRequiredFields(data, requiredFields);
const result = database.save(entityType, data);
logOperation("success", entityType, data.id);
return result;
}
function validateRequiredFields(data, requiredFields) {
for (const field of requiredFields) {
if (!data[field]) {
throw new Error(`${field} is required`);
}
}
}
function logOperation(type, entityType, id) {
const timestamp = new Date().toISOString();
const messages = {
start: `[${timestamp}] Saving ${entityType}: ${id}`,
success: `[${timestamp}] ${entityType} saved successfully: ${id}`,
};
console.log(messages[type]);
}
// 사용법
function saveUser(userData) {
return saveEntity("users", userData, ["email"]);
}
function saveProduct(productData) {
return saveEntity("products", productData, ["name"]);
}
5. 객체지향 리팩터링 패턴
패턴 6: 클래스 추출하기
// 🔧 하나의 클래스가 너무 많은 책임을 가질 때
// BEFORE: 모든 것을 다 하는 클래스
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.orders = [];
}
// 사용자 정보 관리
updateProfile(name, email) {
this.name = name;
this.email = email;
}
// 주문 관리
addOrder(order) {
this.orders.push(order);
}
getTotalSpent() {
return this.orders.reduce((total, order) => total + order.amount, 0);
}
// 이메일 발송
sendWelcomeEmail() {
console.log(`Welcome email sent to ${this.email}`);
}
sendOrderConfirmation(order) {
console.log(`Order confirmation sent to ${this.email}`);
}
// 할인 계산
calculateDiscount() {
const totalSpent = this.getTotalSpent();
if (totalSpent > 10000) return 0.1;
if (totalSpent > 5000) return 0.05;
return 0;
}
}
// AFTER: 책임별로 클래스 분리
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.orderHistory = new OrderHistory();
this.emailService = new EmailService(email);
this.discountCalculator = new DiscountCalculator();
}
updateProfile(name, email) {
this.name = name;
this.email = email;
this.emailService.updateEmail(email);
}
addOrder(order) {
this.orderHistory.addOrder(order);
this.emailService.sendOrderConfirmation(order);
}
getTotalSpent() {
return this.orderHistory.getTotalAmount();
}
getDiscount() {
return this.discountCalculator.calculate(this.getTotalSpent());
}
}
class OrderHistory {
constructor() {
this.orders = [];
}
addOrder(order) {
this.orders.push(order);
}
getTotalAmount() {
return this.orders.reduce((total, order) => total + order.amount, 0);
}
getOrderCount() {
return this.orders.length;
}
}
class EmailService {
constructor(email) {
this.email = email;
}
updateEmail(email) {
this.email = email;
}
sendWelcomeEmail() {
console.log(`Welcome email sent to ${this.email}`);
}
sendOrderConfirmation(order) {
console.log(`Order confirmation sent to ${this.email}`);
}
}
class DiscountCalculator {
calculate(totalSpent) {
if (totalSpent > 10000) return 0.1;
if (totalSpent > 5000) return 0.05;
return 0;
}
}
패턴 7: 상속을 컴포지션으로 바꾸기
// 🔧 복잡한 상속 구조를 컴포지션으로 개선
// BEFORE: 깊은 상속 계층
class Animal {
eat() {
console.log("eating");
}
sleep() {
console.log("sleeping");
}
}
class Mammal extends Animal {
giveBirth() {
console.log("giving birth");
}
}
class Bird extends Animal {
fly() {
console.log("flying");
}
layEggs() {
console.log("laying eggs");
}
}
class FlyingMammal extends Mammal {
fly() {
console.log("flying");
} // 중복 코드
}
class Platypus extends Mammal {
layEggs() {
console.log("laying eggs");
} // 중복 코드
swim() {
console.log("swimming");
}
}
// AFTER: 컴포지션과 믹스인 활용
const behaviors = {
eating: () => console.log("eating"),
sleeping: () => console.log("sleeping"),
flying: () => console.log("flying"),
swimming: () => console.log("swimming"),
givingBirth: () => console.log("giving birth"),
layingEggs: () => console.log("laying eggs"),
};
class Animal {
constructor(name, behaviorList = []) {
this.name = name;
this.behaviors = new Set(behaviorList);
// 행동들을 메서드로 추가
behaviorList.forEach((behavior) => {
if (behaviors[behavior]) {
this[behavior] = behaviors[behavior];
}
});
}
can(behavior) {
return this.behaviors.has(behavior);
}
}
// 사용법
const dog = new Animal("Dog", ["eating", "sleeping", "givingBirth"]);
const bird = new Animal("Bird", ["eating", "sleeping", "flying", "layingEggs"]);
const bat = new Animal("Bat", ["eating", "sleeping", "flying", "givingBirth"]);
const platypus = new Animal("Platypus", ["eating", "sleeping", "swimming", "givingBirth", "layingEggs"]);
// 동적으로 행동 추가도 가능
platypus.addBehavior = function (behavior) {
if (behaviors[behavior]) {
this.behaviors.add(behavior);
this[behavior] = behaviors[behavior];
}
};
6. 함수형 프로그래밍 스타일 리팩터링
패턴 8: 불변성 도입하기
// 🔧 가변 상태를 불변 상태로 전환
// BEFORE: 가변 상태로 인한 부작용
class ShoppingCart {
constructor() {
this.items = [];
this.discounts = [];
}
addItem(item) {
this.items.push(item); // 원본 배열 수정
return this;
}
applyDiscount(discount) {
this.discounts.push(discount); // 원본 배열 수정
return this;
}
removeItem(itemId) {
// 원본 배열에서 직접 제거
this.items = this.items.filter((item) => item.id !== itemId);
return this;
}
calculateTotal() {
let total = this.items.reduce((sum, item) => sum + item.price, 0);
this.discounts.forEach((discount) => {
total -= discount.amount;
});
return total;
}
}
// AFTER: 불변성을 유지하는 설계
class ImmutableShoppingCart {
constructor(items = [], discounts = []) {
this._items = Object.freeze([...items]);
this._discounts = Object.freeze([...discounts]);
}
get items() {
return this._items;
}
get discounts() {
return this._discounts;
}
addItem(item) {
const newItems = [...this._items, item];
return new ImmutableShoppingCart(newItems, this._discounts);
}
removeItem(itemId) {
const newItems = this._items.filter((item) => item.id !== itemId);
return new ImmutableShoppingCart(newItems, this._discounts);
}
applyDiscount(discount) {
const newDiscounts = [...this._discounts, discount];
return new ImmutableShoppingCart(this._items, newDiscounts);
}
calculateTotal() {
const itemsTotal = this._items.reduce((sum, item) => sum + item.price, 0);
const discountTotal = this._discounts.reduce((sum, discount) => sum + discount.amount, 0);
return Math.max(0, itemsTotal - discountTotal);
}
}
// 사용법
let cart = new ImmutableShoppingCart();
cart = cart.addItem({ id: 1, name: "Book", price: 1000 });
cart = cart.addItem({ id: 2, name: "Pen", price: 500 });
cart = cart.applyDiscount({ amount: 100 });
console.log(cart.calculateTotal()); // 1400
패턴 9: 순수 함수로 변환하기
// 🔧 사이드 이펙트가 있는 함수를 순수 함수로 변환
// BEFORE: 사이드 이펙트가 있는 함수들
let globalConfig = {
taxRate: 0.1,
currency: "USD",
};
function calculateTotalPrice(items) {
let total = 0;
for (let item of items) {
total += item.price;
}
// 전역 상태에 의존
total += total * globalConfig.taxRate;
// 외부 서비스 호출 (사이드 이펙트)
logToAnalytics("price_calculated", total);
// 전역 상태 수정
globalConfig.lastCalculation = new Date();
return total;
}
function logToAnalytics(event, data) {
// 외부 API 호출
console.log(`Analytics: ${event} - ${data}`);
}
// AFTER: 순수 함수들로 분리
function calculateTotalPrice(items, config) {
const subtotal = calculateSubtotal(items);
const tax = calculateTax(subtotal, config.taxRate);
return subtotal + tax;
}
function calculateSubtotal(items) {
return items.reduce((total, item) => total + item.price, 0);
}
function calculateTax(amount, taxRate) {
return amount * taxRate;
}
// 사이드 이펙트는 별도 함수로 분리
function logPriceCalculation(total) {
logToAnalytics("price_calculated", total);
}
function updateLastCalculation(config) {
return {
...config,
lastCalculation: new Date(),
};
}
// 사용법 (순수 함수 + 사이드 이펙트 분리)
function processOrder(items, config) {
// 순수 함수로 계산
const total = calculateTotalPrice(items, config);
// 사이드 이펙트는 명시적으로 실행
logPriceCalculation(total);
const updatedConfig = updateLastCalculation(config);
return { total, updatedConfig };
}
7. 비동기 코드 리팩터링
패턴 10: 콜백 지옥 해결하기
// 🔧 콜백 지옥을 Promise와 async/await로 개선
// BEFORE: 콜백 지옥
function processUserOrder(userId, callback) {
getUser(userId, (err, user) => {
if (err) return callback(err);
validateUser(user, (err, isValid) => {
if (err) return callback(err);
if (!isValid) return callback(new Error("Invalid user"));
getOrderHistory(userId, (err, orders) => {
if (err) return callback(err);
calculateDiscount(user, orders, (err, discount) => {
if (err) return callback(err);
createOrder(user, discount, (err, order) => {
if (err) return callback(err);
sendConfirmationEmail(user.email, order, (err) => {
if (err) return callback(err);
callback(null, order);
});
});
});
});
});
});
}
// AFTER: async/await로 깔끔하게
async function processUserOrder(userId) {
try {
const user = await getUser(userId);
const isValid = await validateUser(user);
if (!isValid) {
throw new Error("Invalid user");
}
const orders = await getOrderHistory(userId);
const discount = await calculateDiscount(user, orders);
const order = await createOrder(user, discount);
// 이메일 발송은 백그라운드에서 (필요시)
sendConfirmationEmail(user.email, order).catch(console.error);
return order;
} catch (error) {
console.error("Order processing failed:", error);
throw error;
}
}
// Promise 기반 헬퍼 함수들
function getUser(userId) {
return new Promise((resolve, reject) => {
// DB 조회 로직
setTimeout(() => resolve({ id: userId, email: "user@example.com" }), 100);
});
}
function validateUser(user) {
return new Promise((resolve) => {
// 검증 로직
setTimeout(() => resolve(!!user.email), 50);
});
}
// 더 나은 방법: 병렬 처리가 가능한 부분 최적화
async function processUserOrderOptimized(userId) {
try {
const user = await getUser(userId);
// 병렬로 실행 가능한 작업들
const [isValid, orders] = await Promise.all([validateUser(user), getOrderHistory(userId)]);
if (!isValid) {
throw new Error("Invalid user");
}
const discount = await calculateDiscount(user, orders);
const order = await createOrder(user, discount);
// 백그라운드 작업
sendConfirmationEmail(user.email, order).catch(console.error);
return order;
} catch (error) {
console.error("Order processing failed:", error);
throw error;
}
}
8. 에러 처리 리팩터링
패턴 11: 에러 처리 개선하기
// 🔧 중복되는 에러 처리 로직을 깔끔하게 정리
// BEFORE: 각 함수마다 반복되는 에러 처리
async function getUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error("User not found");
} else if (response.status === 401) {
throw new Error("Unauthorized");
} else {
throw new Error("Server error");
}
}
return await response.json();
} catch (error) {
console.error("Error fetching user:", error);
throw error;
}
}
async function getOrders(userId) {
try {
const response = await fetch(`/api/users/${userId}/orders`);
if (!response.ok) {
if (response.status === 404) {
throw new Error("Orders not found");
} else if (response.status === 401) {
throw new Error("Unauthorized");
} else {
throw new Error("Server error");
}
}
return await response.json();
} catch (error) {
console.error("Error fetching orders:", error);
throw error;
}
}
// AFTER: 공통 에러 처리 로직 추출
class APIError extends Error {
constructor(message, status, originalError) {
super(message);
this.name = "APIError";
this.status = status;
this.originalError = originalError;
}
}
class APIClient {
async request(url, options = {}) {
try {
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
...options.headers,
},
...options,
});
if (!response.ok) {
throw new APIError(this.getErrorMessage(response.status), response.status);
}
return await response.json();
} catch (error) {
if (error instanceof APIError) {
throw error;
}
throw new APIError("Network error", 0, error);
}
}
getErrorMessage(status) {
const errorMessages = {
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
500: "Internal Server Error",
};
return errorMessages[status] || "Unknown Error";
}
async get(endpoint) {
return this.request(endpoint);
}
async post(endpoint, data) {
return this.request(endpoint, {
method: "POST",
body: JSON.stringify(data),
});
}
}
// 사용법
const apiClient = new APIClient();
async function getUser(id) {
try {
return await apiClient.get(`/api/users/${id}`);
} catch (error) {
if (error.status === 404) {
return null; // 사용자 없음을 null로 표현
}
throw error; // 다른 에러는 상위로 전파
}
}
async function getOrders(userId) {
try {
return await apiClient.get(`/api/users/${userId}/orders`);
} catch (error) {
if (error.status === 404) {
return []; // 주문 없음을 빈 배열로 표현
}
throw error;
}
}
// 더 고급: Result 패턴 적용
class Result {
constructor(data, error) {
this.data = data;
this.error = error;
}
static success(data) {
return new Result(data, null);
}
static failure(error) {
return new Result(null, error);
}
isSuccess() {
return this.error === null;
}
isFailure() {
return this.error !== null;
}
}
async function getUserSafe(id) {
try {
const user = await apiClient.get(`/api/users/${id}`);
return Result.success(user);
} catch (error) {
return Result.failure(error);
}
}
// 사용법
const userResult = await getUserSafe(123);
if (userResult.isSuccess()) {
console.log("User:", userResult.data);
} else {
console.error("Error:", userResult.error.message);
}
9. 리팩터링 실전 전략
단계별 리팩터링 접근법
# 📋 리팩터링 프로세스
1. 현재 상태 파악
├── 코드 리뷰 및 문제점 식별
├── 기존 테스트 코드 확인
└── 비즈니스 로직 이해
2. 테스트 커버리지 확보
├── 기존 기능에 대한 테스트 작성
├── Edge case 테스트 추가
└── 리팩터링 전 모든 테스트 통과 확인
3. 점진적 개선
├── 작은 단위로 리팩터링
├── 각 단계마다 테스트 실행
└── 커밋 단위로 진행 상황 저장
4. 검증 및 정리
├── 성능 테스트 실행
├── 코드 리뷰 요청
└── 문서 업데이트
리팩터링 우선순위 결정
// 🎯 어떤 코드부터 리팩터링할지 결정하기
const refactoringPriority = {
// 높은 우선순위 (즉시 리팩터링)
high: ["보안 취약점이 있는 코드", "버그가 자주 발생하는 코드", "성능 문제가 있는 코드", "새 기능 추가가 필요한 부분"],
// 중간 우선순위 (계획적 리팩터링)
medium: ["테스트하기 어려운 코드", "중복이 많은 코드", "이해하기 어려운 복잡한 코드", "확장성이 부족한 코드"],
// 낮은 우선순위 (여유가 있을 때)
low: ["네이밍이 명확하지 않은 코드", "주석이 부족한 코드", "스타일 가이드에 맞지 않는 코드", "레거시 패턴을 사용하는 코드"],
};
// 코드 품질 측정 지표
function assessCodeQuality(file) {
return {
complexity: getCyclomaticComplexity(file),
testCoverage: getTestCoverage(file),
duplication: getDuplicationRate(file),
maintainabilityIndex: getMaintainabilityIndex(file),
bugDensity: getBugDensity(file),
};
}
팀 단위 리팩터링 전략
## 🤝 팀 리팩터링 가이드라인
### 리팩터링 규칙
1. **보이스카웃 규칙**: 코드를 건드릴 때마다 조금씩 개선
2. **2인 규칙**: 큰 리팩터링은 최소 2명이 함께 진행
3. **시간 박스**: 리팩터링 시간을 미리 정해두고 진행
4. **피처 플래그**: 큰 변경사항은 피처 플래그로 안전하게
### 코드 리뷰 체크리스트
- [ ] 기존 기능이 그대로 동작하는가?
- [ ] 테스트 커버리지가 유지되거나 개선되었는가?
- [ ] 성능이 저하되지 않았는가?
- [ ] 코드가 더 이해하기 쉬워졌는가?
- [ ] 새로운 버그를 도입하지 않았는가?
### 커뮤니케이션
- 리팩터링 계획을 팀원들과 공유
- 진행 상황을 주기적으로 업데이트
- 문제 발생 시 즉시 팀원들에게 알림
- 완료 후 개선된 점을 팀원들과 공유
10. 리팩터링 도구와 자동화
정적 분석 도구 활용
// 🛠️ ESLint 설정으로 리팩터링 가이드
// .eslintrc.js
module.exports = {
extends: ["eslint:recommended"],
rules: {
// 복잡도 제한
complexity: ["error", { max: 10 }],
"max-depth": ["error", 4],
"max-lines-per-function": ["error", { max: 50 }],
// 네이밍 규칙
camelcase: "error",
"no-underscore-dangle": "error",
// 코드 품질
"no-magic-numbers": ["error", { ignore: [0, 1, -1] }],
"prefer-const": "error",
"no-var": "error",
// 함수형 프로그래밍 권장
"prefer-arrow-callback": "error",
"no-loop-func": "error",
},
};
// SonarQube 품질 게이트 설정
const qualityGate = {
coverage: "> 80%",
duplicatedLines: "< 3%",
maintainabilityRating: "A",
reliabilityRating: "A",
securityRating: "A",
};
자동 리팩터링 도구
# 🤖 자동 리팩터링 도구들
# 1. jscodeshift (JavaScript 코드 변환)
npx jscodeshift -t transforms/arrow-functions.js src/
# 2. Prettier (코드 포매팅)
npx prettier --write "src/**/*.js"
# 3. TypeScript 마이그레이션
npx typescript-migrate --init
npx typescript-migrate --migrate
# 4. 의존성 업데이트
npx npm-check-updates -u
npm install
# 5. 사용하지 않는 코드 제거
npx unimported
npx depcheck
VS Code 확장 도구들
// settings.json - 리팩터링에 도움되는 설정
{
"editor.codeActionsOnSave": {
"source.organizeImports": true,
"source.fixAll.eslint": true
},
"typescript.suggest.autoImports": true,
"javascript.suggest.autoImports": true,
"editor.formatOnSave": true,
"files.autoSave": "onFocusChange"
}
// 추천 확장 도구들
const recommendedExtensions = [
'esbenp.prettier-vscode', // 자동 포매팅
'dbaeumer.vscode-eslint', // 린팅
'formulahendry.auto-rename-tag', // HTML 태그 자동 리네임
'bradlc.vscode-tailwindcss', // CSS 클래스 자동완성
'ms-vscode.vscode-typescript-next' // 고급 TypeScript 지원
];
11. 성과 측정과 지속적 개선
리팩터링 효과 측정
// 📊 리팩터링 전후 비교 지표
const refactoringMetrics = {
before: {
cyclomaticComplexity: 15, // 복잡도
linesOfCode: 1200, // 코드 라인 수
testCoverage: 45, // 테스트 커버리지 (%)
bugCount: 23, // 버그 수 (월별)
deploymentFrequency: 2, // 배포 빈도 (월별)
developmentVelocity: 18, // 개발 속도 (스토리 포인트/스프린트)
},
after: {
cyclomaticComplexity: 8, // 47% 감소
linesOfCode: 800, // 33% 감소
testCoverage: 82, // 82% 증가
bugCount: 7, // 70% 감소
deploymentFrequency: 8, // 300% 증가
developmentVelocity: 28, // 56% 증가
},
};
// ROI 계산
function calculateRefactoringROI(metrics) {
const timeSaved = (metrics.before.bugCount - metrics.after.bugCount) * 2; // 버그당 2시간 절약
const velocityGain = metrics.after.developmentVelocity - metrics.before.developmentVelocity;
const refactoringCost = 40; // 리팩터링에 투입된 시간
const roi = ((timeSaved + velocityGain) / refactoringCost - 1) * 100;
return Math.round(roi);
}
console.log(`리팩터링 ROI: ${calculateRefactoringROI(refactoringMetrics)}%`);
지속적인 개선 프로세스
# 🔄 지속적 리팩터링 워크플로우
# 주간 코드 품질 체크
npm run lint
npm run test:coverage
npm run analyze:complexity
# 월간 기술 부채 리뷰
git log --since="1 month ago" --grep="fix|hack|todo"
npm audit
npx license-checker
# 분기별 아키텍처 리뷰
npm run bundle-analyzer
npx madge --circular src/
npx dependency-cruiser src
마치며: 리팩터링은 마라톤이다
리팩터링 성공을 위한 핵심 원칙
## 🎯 리팩터링 성공의 비밀
### 1. 작게, 자주, 꾸준히
- 매일 조금씩 개선하는 습관
- 큰 변경보다는 작은 개선의 누적
- 완벽함보다는 지속가능성 추구
### 2. 테스트는 안전망
- 리팩터링 전 반드시 테스트 코드 작성
- 테스트 없는 리팩터링은 도박
- 자동화된 테스트로 신뢰성 확보
### 3. 팀과 함께
- 혼자만의 리팩터링은 독선
- 코드 리뷰를 통한 지식 공유
- 팀 전체의 코드 품질 의식 향상
### 4. 비즈니스 가치 우선
- 기술적 완벽함보다는 실용성
- 사용자 가치를 해치지 않는 범위에서
- 개발 생산성 향상이 최종 목표
단계별 성장 로드맵
초급 (1-3개월):
- [x] 함수 추출하기, 변수 추출하기 연습
- [x] 매직 넘버/문자열 상수화
- [x] 간단한 조건문 개선
- [x] 기본 테스트 코드 작성
중급 (3-6개월):
- [x] 클래스와 모듈 단위 리팩터링
- [x] 디자인 패턴 적용
- [x] 비동기 코드 개선
- [x] 성능 최적화
고급 (6개월+):
- [x] 아키텍처 수준 리팩터링
- [x] 레거시 시스템 현대화
- [x] 팀 리팩터링 프로세스 구축
- [x] 자동화 도구 구축
실무에서 바로 적용할 수 있는 체크리스트
## 📋 일일 리팩터링 체크리스트
### 코드 작성 시
- [ ] 함수가 한 가지 일만 하는가?
- [ ] 변수명이 의도를 명확하게 표현하는가?
- [ ] 중복된 코드가 없는가?
- [ ] 복잡한 조건문을 간소화할 수 있는가?
### 코드 리뷰 시
- [ ] 가독성이 좋은가?
- [ ] 테스트하기 쉬운 구조인가?
- [ ] 확장하기 쉬운 설계인가?
- [ ] 성능상 문제는 없는가?
### 주간 점검
- [ ] 코드 복잡도 지표 확인
- [ ] 테스트 커버리지 점검
- [ ] 기술 부채 목록 업데이트
- [ ] 리팩터링 우선순위 재정렬
마지막 조언
리팩터링은 코드를 예쁘게 만드는 것이 아닙니다. 더 나은 소프트웨어를 더 빠르게 만들기 위한 전략적 투자입니다.
“완벽한 코드”를 추구하기보다는 “지속적으로 개선 가능한 코드”를 만드는 것이 중요해요. 매일 조금씩, 꾸준히, 팀원들과 함께 개선해 나가다 보면 어느새 놀라운 변화를 경험하게 될 거예요.
가장 중요한 것은 실행입니다. 오늘 당장 작성하고 있는 코드에서 하나의 함수라도 추출해보세요. 변수명 하나라도 더 명확하게 바꿔보세요. 작은 시작이 큰 변화의 출발점이 됩니다! 🚀
다음에는 “Clean Code 실전 가이드: 읽기 쉬운 코드 작성법”으로 더 깊이 있는 코드 품질 향상 방법을 다뤄보겠습니다. 기대해 주세요!