실무에서 자주 쓰는 리팩터링 패턴: 레거시 코드를 현대적으로 개선하는 실전 가이드

안녕하세요, 성장하는 개발자 여러분!

“이 코드 누가 짰어?” 😱

실무에서 가장 많이 듣는 말 중 하나입니다. 하지만 놀랍게도 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);
}

리팩터링의 핵심 원칙

  1. 겉보기 동작은 그대로 유지: 외부에서 보는 결과는 동일해야 함
  2. 내부 구조만 개선: 가독성, 유지보수성, 확장성 향상
  3. 작은 단계로 진행: 한 번에 하나씩, 안전하게
  4. 테스트 주도: 리팩터링 전후 테스트로 검증

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 실전 가이드: 읽기 쉬운 코드 작성법”으로 더 깊이 있는 코드 품질 향상 방법을 다뤄보겠습니다. 기대해 주세요!

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다