[태그:] Refactoring

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

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

    “이 코드 누가 짰어?” 😱

    실무에서 가장 많이 듣는 말 중 하나입니다. 하지만 놀랍게도 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 실전 가이드: 읽기 쉬운 코드 작성법”으로 더 깊이 있는 코드 품질 향상 방법을 다뤄보겠습니다. 기대해 주세요!