IT Knowledge/Development/images/리팩토링-기법-diagram.svg

📌 핵심 개념

쉬운 비유: 방 청소. 물건 위치를 바꾸거나 정리하지만, 물건 자체(기능)는 그대로입니다. 깔끔해져서 찾기 쉬워집니다.

리팩토링 = 동작은 그대로 + 코드 구조 개선

AI 요약 보고서

  • 리팩토링 개념: 기존 동작은 유지하면서 코드 구조를 개선하는 작업으로, 가독성 향상, 재사용성 증대, 테스트 용이성을 목적으로 수행됨. 비유하자면 방 청소와 유사하게, 물건 위치를 정리하는 것임.

  • 주요 기법:

    • Extract Function: 긴 함수에서 기능별로 작은 함수로 분리하여 가독성 향상. 예시에서는 주문 처리 과정을 hasEnoughStock, calculateDiscount, calculateTotal, processPayment 등으로 분리.
    • Replace Magic Number: 의미 없는 숫자를 상수로 치환하여 의미 부여. 예를 들어, 배송비와 타이머 시간 등을 명확한 상수(SHIPPING_COST, FIVE_MINUTES)로 변경.
    • Remove Dead Code: 사용하지 않는 코드와 주석을 제거하여 코드 정리. 버전 관리 시스템 활용 권장.
    • Simplify Conditional: 복잡한 조건문을 조기 반환 또는 규칙 기반 구조로 간소화. 예시에서는 할인율 계산을 명확한 조건문 또는 객체로 재구성.
    • Extract Variable: 복잡한 표현식을 의미 있는 변수로 분리하여 가독성 향상. 예를 들어, subtotal, discountRate, shippingFee 등.
    • Replace Loop with Pipeline: 명령형 루프를 선언적 함수(filter, map, reduce)로 대체하여 가독성 및 유지보수성 향상.
  • 실천 원칙:

    • 테스트 우선: 리팩토링 전후 동작 검증을 위한 테스트 작성 후 단계별 개선.
    • 작은 단계: 한 번에 전체가 아닌 작은 함수 또는 조건문부터 차근차근 개선.
    • 자주 커밋: 변경 사항마다 커밋하여 변경 이력 명확히 유지.
  • 효과: 코드의 명료성, 재사용성, 유지보수성 증대, 버그 방지 가능. 참고 자료로 Martin Fowler의 RefactoringRefactoring Guru 추천.

🎯 실전 기법

1. Extract Function (함수 추출)

Before:

// ❌ 하나의 긴 함수 (100줄)
function processOrder(order) {
  // 재고 확인
  let hasStock = true;
  for (const item of order.items) {
    const product = db.products.findById(item.productId);
    if (product.stock < item.quantity) {
      hasStock = false;
      break;
    }
  }
 
  if (!hasStock) {
    return { success: false, error: '재고 부족' };
  }
 
  // 할인 계산
  let discount = 0;
  if (order.couponCode) {
    const coupon = db.coupons.findByCode(order.couponCode);
    if (coupon.type === 'PERCENTAGE') {
      discount = order.subtotal * (coupon.value / 100);
    } else {
      discount = coupon.value;
    }
  }
 
  // 최종 금액
  const total = order.subtotal - discount + order.shippingFee;
 
  // 결제 처리
  const payment = paymentGateway.charge(order.userId, total);
 
  // ...더 많은 로직
}

After:

// ✅ 작은 함수들로 분리
function processOrder(order) {
  if (!hasEnoughStock(order.items)) {
    return { success: false, error: '재고 부족' };
  }
 
  const discount = calculateDiscount(order);
  const total = calculateTotal(order, discount);
  const payment = processPayment(order.userId, total);
 
  return { success: true, payment };
}
 
function hasEnoughStock(items) {
  return items.every(item => {
    const product = db.products.findById(item.productId);
    return product.stock >= item.quantity;
  });
}
 
function calculateDiscount(order) {
  if (!order.couponCode) return 0;
 
  const coupon = db.coupons.findByCode(order.couponCode);
 
  return coupon.type === 'PERCENTAGE'
    ? order.subtotal * (coupon.value / 100)
    : coupon.value;
}
 
function calculateTotal(order, discount) {
  return order.subtotal - discount + order.shippingFee;
}
 
function processPayment(userId, amount) {
  return paymentGateway.charge(userId, amount);
}

효과:

  • 가독성 ⬆️ (한눈에 파악)
  • 재사용 가능
  • 테스트 쉬움

2. Replace Magic Number (매직 넘버 제거)

Before:

// ❌ 숫자의 의미를 알 수 없음
function calculateShipping(weight) {
  if (weight < 5) {
    return 3000;
  } else if (weight < 20) {
    return 5000;
  } else {
    return 10000;
  }
}
 
setTimeout(() => {
  checkStatus();
}, 300000);  // 300000이 뭐지?

After:

// ✅ 의미 있는 이름
const SHIPPING_COST = {
  LIGHT: 3000,      // 5kg 미만
  MEDIUM: 5000,     // 5-20kg
  HEAVY: 10000      // 20kg 이상
};
 
const WEIGHT_THRESHOLD = {
  LIGHT: 5,
  MEDIUM: 20
};
 
function calculateShipping(weight) {
  if (weight < WEIGHT_THRESHOLD.LIGHT) {
    return SHIPPING_COST.LIGHT;
  } else if (weight < WEIGHT_THRESHOLD.MEDIUM) {
    return SHIPPING_COST.MEDIUM;
  } else {
    return SHIPPING_COST.HEAVY;
  }
}
 
const FIVE_MINUTES = 5 * 60 * 1000;
setTimeout(() => {
  checkStatus();
}, FIVE_MINUTES);

3. Remove Dead Code (죽은 코드 제거)

Before:

// ❌ 사용하지 않는 코드
function processUser(user) {
  // const oldWay = computeOldWay(user);  // 주석 처리된 코드
 
  const result = computeNewWay(user);
 
  // if (DEBUG_MODE) {  // 사용 안 하는 조건
  //   console.log('Debug:', result);
  // }
 
  return result;
}
 
// function computeOldWay(user) {  // 더 이상 사용 안 함
//   return user.name + user.age;
// }

After:

// ✅ 깔끔
function processUser(user) {
  return computeNewWay(user);
}
 
// Git 히스토리에 있으니 필요하면 복원 가능
// 주석 처리하지 말고 삭제!

4. Simplify Conditional (조건문 간소화)

Before:

// ❌ 복잡한 조건문
function getDiscount(user, order) {
  if (user.isPremium) {
    if (order.total > 100000) {
      if (user.purchaseCount > 10) {
        return 0.3;  // 30% 할인
      } else {
        return 0.2;  // 20% 할인
      }
    } else {
      return 0.1;  // 10% 할인
    }
  } else {
    if (order.total > 50000) {
      return 0.05;  // 5% 할인
    } else {
      return 0;  // 할인 없음
    }
  }
}

After:

// ✅ Early Return으로 단순화
function getDiscount(user, order) {
  // 일반 회원
  if (!user.isPremium) {
    return order.total > 50000 ? 0.05 : 0;
  }
 
  // 프리미엄 회원
  if (order.total <= 100000) {
    return 0.1;
  }
 
  return user.purchaseCount > 10 ? 0.3 : 0.2;
}
 
// 또는 객체로
const DISCOUNT_RULES = {
  premium: {
    vip: 0.3,        // 10회 이상 구매 + 10만원 이상
    regular: 0.2,    // 10만원 이상
    basic: 0.1       // 10만원 미만
  },
  normal: {
    high: 0.05,      // 5만원 이상
    basic: 0         // 5만원 미만
  }
};
 
function getDiscount(user, order) {
  if (!user.isPremium) {
    return order.total > 50000
      ? DISCOUNT_RULES.normal.high
      : DISCOUNT_RULES.normal.basic;
  }
 
  if (order.total <= 100000) {
    return DISCOUNT_RULES.premium.basic;
  }
 
  return user.purchaseCount > 10
    ? DISCOUNT_RULES.premium.vip
    : DISCOUNT_RULES.premium.regular;
}

5. Extract Variable (변수 추출)

Before:

// ❌ 복잡한 표현식
function calculatePrice(order) {
  return (
    order.items.reduce((sum, item) => sum + item.price * item.quantity, 0) *
    (1 - (order.coupon ? order.coupon.discount / 100 : 0)) +
    (order.items.reduce((sum, item) => sum + item.weight, 0) > 5 ? 5000 : 3000)
  );
}

After:

// ✅ 의미 있는 변수로 분리
function calculatePrice(order) {
  const subtotal = order.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
 
  const discountRate = order.coupon ? order.coupon.discount / 100 : 0;
  const discountedPrice = subtotal * (1 - discountRate);
 
  const totalWeight = order.items.reduce(
    (sum, item) => sum + item.weight,
    0
  );
  const shippingFee = totalWeight > 5 ? 5000 : 3000;
 
  return discountedPrice + shippingFee;
}

6. Replace Loop with Pipeline (루프를 파이프라인으로)

Before:

// ❌ 명령형 루프
function getActiveUserNames(users) {
  const result = [];
 
  for (const user of users) {
    if (user.isActive) {
      result.push(user.name);
    }
  }
 
  return result;
}

After:

// ✅ 선언적 파이프라인
function getActiveUserNames(users) {
  return users
    .filter(user => user.isActive)
    .map(user => user.name);
}
 
// 더 복잡한 예시
function getTopProducts(orders) {
  return orders
    .flatMap(order => order.items)  // 모든 주문 아이템
    .reduce((acc, item) => {
      acc[item.productId] = (acc[item.productId] || 0) + item.quantity;
      return acc;
    }, {})  // 상품별 총 수량
    .entries()
    .sort((a, b) => b[1] - a[1])  // 수량순 정렬
    .slice(0, 10);  // 상위 10개
}

💡 리팩토링 원칙

1. 테스트 먼저

// 1. 기존 동작 테스트 작성
test('기존 calculatePrice 동작', () => {
  const order = {
    items: [{ price: 10000, quantity: 2 }],
    coupon: { discount: 10 }
  };
 
  expect(calculatePrice(order)).toBe(18000);
});
 
// 2. 리팩토링
 
// 3. 테스트가 여전히 통과하는지 확인

2. 작은 단계로

1회: 함수 하나 추출
2회: 변수명 개선
3회: 조건문 간소화
...

❌ 한 번에 전체 리팩토링 (위험)
✅ 조금씩 개선 (안전)

3. 커밋 자주

git commit -m "Extract hasEnoughStock function"
git commit -m "Replace magic numbers with constants"
git commit -m "Simplify discount calculation"

🔗 참고 자료