443 lines
9.3 KiB
Markdown
443 lines
9.3 KiB
Markdown
---
|
|
id: quality-refactoring
|
|
title: Refactoring — 작은 step / 안전 변경
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [quality, refactoring, vibe-coding]
|
|
tech_stack: { language: "Various", applicable_to: ["Engineering"] }
|
|
applied_in: []
|
|
aliases: [refactoring, extract function, rename, move, code smell, inline, parallel change]
|
|
---
|
|
|
|
# Refactoring
|
|
|
|
> **작은 reversible step**. 매 step 후 test. Behavior 변경 X — 단지 structure. Martin Fowler 의 카탈로그 + IDE refactoring tools.
|
|
|
|
## 📖 핵심 개념
|
|
- Code smell: 변경 신호.
|
|
- Extract / Inline / Move / Rename.
|
|
- Test first — 안전망.
|
|
- Parallel change — 큰 변경 점진.
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### 가장 일반 refactoring
|
|
```
|
|
1. Extract function/method: 긴 function → 작은 function
|
|
2. Inline: 과도한 indirection 제거
|
|
3. Rename: 의미 명확
|
|
4. Move: 적절 위치
|
|
5. Extract variable: 복잡 expression 명명
|
|
6. Replace conditional with polymorphism
|
|
7. Replace magic number with constant
|
|
8. Encapsulate field
|
|
```
|
|
|
|
### Extract function (가장 자주)
|
|
```ts
|
|
// Before
|
|
function processOrder(order: Order) {
|
|
// Calculate tax (10 lines)
|
|
let tax = 0;
|
|
for (const item of order.items) {
|
|
tax += item.price * 0.08;
|
|
}
|
|
|
|
// Calculate shipping (15 lines)
|
|
let shipping = 0;
|
|
if (order.weight > 10) shipping = 20;
|
|
else if (order.weight > 5) shipping = 10;
|
|
else shipping = 5;
|
|
|
|
// Apply discount (8 lines)
|
|
// ...
|
|
|
|
return order.subtotal + tax + shipping - discount;
|
|
}
|
|
|
|
// After
|
|
function processOrder(order: Order) {
|
|
const tax = calculateTax(order.items);
|
|
const shipping = calculateShipping(order.weight);
|
|
const discount = applyDiscount(order);
|
|
return order.subtotal + tax + shipping - discount;
|
|
}
|
|
|
|
function calculateTax(items: Item[]): number {
|
|
return items.reduce((sum, i) => sum + i.price * 0.08, 0);
|
|
}
|
|
|
|
function calculateShipping(weight: number): number {
|
|
if (weight > 10) return 20;
|
|
if (weight > 5) return 10;
|
|
return 5;
|
|
}
|
|
```
|
|
|
|
→ 의미 명명 + test 가능.
|
|
|
|
### Replace conditional with polymorphism
|
|
```ts
|
|
// Before
|
|
class Animal {
|
|
sound(): string {
|
|
if (this.type === 'dog') return 'Woof';
|
|
if (this.type === 'cat') return 'Meow';
|
|
if (this.type === 'cow') return 'Moo';
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// After
|
|
abstract class Animal {
|
|
abstract sound(): string;
|
|
}
|
|
class Dog extends Animal { sound() { return 'Woof'; } }
|
|
class Cat extends Animal { sound() { return 'Meow'; } }
|
|
class Cow extends Animal { sound() { return 'Moo'; } }
|
|
```
|
|
|
|
또는 lookup table:
|
|
```ts
|
|
const sounds: Record<AnimalType, string> = {
|
|
dog: 'Woof', cat: 'Meow', cow: 'Moo',
|
|
};
|
|
function sound(type: AnimalType): string {
|
|
return sounds[type];
|
|
}
|
|
```
|
|
|
|
### Parallel change (큰 변경)
|
|
```
|
|
3 phase:
|
|
1. Expand: New 가 추가됨 (old 도 keep)
|
|
2. Migrate: 사용자가 new 로 이동
|
|
3. Contract: Old 제거
|
|
|
|
각 phase = deployable.
|
|
```
|
|
|
|
```ts
|
|
// Phase 1: Add new method
|
|
class API {
|
|
oldMethod(x: number) { return x * 2; }
|
|
newMethod(x: number, y: number) { return x * 2 + y; }
|
|
}
|
|
|
|
// Phase 2: Migrate callers (PR by PR)
|
|
api.oldMethod(5) → api.newMethod(5, 0)
|
|
|
|
// Phase 3: Remove old method
|
|
class API {
|
|
newMethod(x: number, y: number) { ... }
|
|
}
|
|
```
|
|
|
|
→ Big bang 보다 안전.
|
|
|
|
### Strangler fig
|
|
```
|
|
큰 system 의 일부 점진 교체:
|
|
|
|
Phase 1: Old system 그대로.
|
|
Phase 2: New system + facade — 일부 traffic.
|
|
Phase 3: 점진 traffic 이동.
|
|
Phase 4: Old system 제거.
|
|
```
|
|
|
|
```ts
|
|
// Facade
|
|
class OrderService {
|
|
async create(order: Order) {
|
|
if (featureFlag('use-new-system')) {
|
|
return newSystem.create(order);
|
|
}
|
|
return oldSystem.create(order);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Code smells (refactor 신호)
|
|
```
|
|
1. Long method (50+ lines): extract function
|
|
2. Large class (500+ lines): extract / split
|
|
3. Long parameter list (5+): parameter object
|
|
4. Duplicate code: extract + reuse
|
|
5. Switch statements: polymorphism / lookup
|
|
6. Feature envy (다른 class 의 데이터): move method
|
|
7. Data clumps (같은 그룹 매번): data class
|
|
8. Primitive obsession: value object
|
|
9. Comments explaining what: extract function (이름 붙임)
|
|
10. Dead code: delete
|
|
```
|
|
|
|
### Test first
|
|
```ts
|
|
// 1. Test 작성 (현재 behavior)
|
|
test('processOrder calculates correctly', () => {
|
|
const order = makeOrder(...);
|
|
expect(processOrder(order)).toBe(95.20);
|
|
});
|
|
|
|
// 2. Refactor
|
|
function processOrder(order: Order) {
|
|
const tax = calculateTax(order.items);
|
|
// ...
|
|
}
|
|
|
|
// 3. Test 통과 = behavior 같음
|
|
```
|
|
|
|
### IDE refactoring (안전)
|
|
```
|
|
VS Code / WebStorm:
|
|
- F2: Rename (모든 reference)
|
|
- Ctrl+Shift+R: Extract method
|
|
- Ctrl+Alt+M: Extract variable
|
|
- Move file (drag)
|
|
- Auto-import organize
|
|
|
|
→ Manual 보다 안전 + 빠름.
|
|
```
|
|
|
|
### TS 의 점진 typing
|
|
```ts
|
|
// Phase 1: any 만
|
|
function process(input: any): any { ... }
|
|
|
|
// Phase 2: 입력 type
|
|
function process(input: Order): any { ... }
|
|
|
|
// Phase 3: 출력 type
|
|
function process(input: Order): Result { ... }
|
|
|
|
// Phase 4: 내부 변수 type
|
|
```
|
|
|
|
→ 점진 strict.
|
|
|
|
### Inline (과도 indirection 제거)
|
|
```ts
|
|
// Before
|
|
function getEmail(user: User): string {
|
|
return user.email;
|
|
}
|
|
const email = getEmail(user); // 의미 없는 wrapping
|
|
|
|
// After
|
|
const email = user.email;
|
|
```
|
|
|
|
### Rename (semantic)
|
|
```ts
|
|
// Before
|
|
function calc(x: number, y: number) { ... }
|
|
|
|
// After
|
|
function calculateTotalPrice(quantity: number, unitPrice: number) { ... }
|
|
```
|
|
|
|
→ 의미 명확.
|
|
|
|
### Extract variable
|
|
```ts
|
|
// Before
|
|
if (order.items.length > 10 && order.totalAmount > 1000 && order.user.tier === 'gold') {
|
|
// ...
|
|
}
|
|
|
|
// After
|
|
const isLargeOrder = order.items.length > 10;
|
|
const isHighValue = order.totalAmount > 1000;
|
|
const isVipUser = order.user.tier === 'gold';
|
|
|
|
if (isLargeOrder && isHighValue && isVipUser) {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### Encapsulate field
|
|
```ts
|
|
// Before
|
|
class User {
|
|
email: string; // 누가나 변경 가능
|
|
}
|
|
user.email = '...'; // validation 없음
|
|
|
|
// After
|
|
class User {
|
|
#email: string;
|
|
|
|
get email(): string { return this.#email; }
|
|
|
|
setEmail(value: string) {
|
|
if (!isValid(value)) throw new Error('invalid');
|
|
this.#email = value;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Replace magic number
|
|
```ts
|
|
// Before
|
|
if (statusCode === 429) { ... }
|
|
|
|
// After
|
|
const TOO_MANY_REQUESTS = 429;
|
|
if (statusCode === TOO_MANY_REQUESTS) { ... }
|
|
|
|
// 또는
|
|
import { StatusCode } from './http-status';
|
|
if (statusCode === StatusCode.TooManyRequests) { ... }
|
|
```
|
|
|
|
### Move method to better class
|
|
```ts
|
|
// Before — Order class 가 customer 데이터 사용
|
|
class Order {
|
|
getCustomerDiscount() {
|
|
return this.customer.tier === 'gold' ? 0.1 : 0;
|
|
}
|
|
}
|
|
|
|
// After — Customer class 안
|
|
class Customer {
|
|
getDiscount() {
|
|
return this.tier === 'gold' ? 0.1 : 0;
|
|
}
|
|
}
|
|
```
|
|
|
|
→ Feature envy 제거.
|
|
|
|
### Replace inheritance with composition
|
|
```ts
|
|
// Before
|
|
class Reader extends FileSystem { ... }
|
|
|
|
// After
|
|
class Reader {
|
|
constructor(private fs: FileSystem) { ... }
|
|
}
|
|
```
|
|
|
|
### Simplifying conditionals
|
|
```ts
|
|
// Before
|
|
if (x) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
// After
|
|
return Boolean(x);
|
|
|
|
// Before
|
|
if (x === null || x === undefined) return defaultVal;
|
|
return x;
|
|
|
|
// After
|
|
return x ?? defaultVal;
|
|
```
|
|
|
|
### Refactor in sprint
|
|
```
|
|
Friday: refactor day.
|
|
또는 매 PR 의 작은 cleanup.
|
|
|
|
큰 refactor = 별 sprint 또는 분기.
|
|
```
|
|
|
|
### Continuous refactoring
|
|
```
|
|
Boy scout rule + 매 PR + IDE 도구 = 코드 점차 개선.
|
|
|
|
Big-bang refactor 거의 안 함.
|
|
```
|
|
|
|
### When NOT to refactor
|
|
```
|
|
- 작동하는 코드 + 안 변경할 영역
|
|
- Critical path + test 없음
|
|
- Deadline 임박
|
|
- 이해 안 됐을 때 (먼저 이해)
|
|
- 명확 owner X
|
|
```
|
|
|
|
→ Refactor = 변경 시 우선.
|
|
|
|
### Communication
|
|
```
|
|
PR description:
|
|
"This PR is pure refactor — no behavior change.
|
|
Reduces complexity score from 25 → 12."
|
|
|
|
→ Reviewer 가 변경 검사.
|
|
```
|
|
|
|
### Test gap (refactor 전)
|
|
```
|
|
Test 없으면:
|
|
1. 작은 black-box test (입력/출력)
|
|
2. Refactor
|
|
3. Test 가 behavior lock
|
|
```
|
|
|
|
### Tools
|
|
```
|
|
- VS Code / WebStorm refactoring
|
|
- ESLint --fix
|
|
- ts-prune (unused exports)
|
|
- knip (unused files / deps)
|
|
- prettier (style — refactor 의 friend)
|
|
- AI assist (Cursor, Copilot)
|
|
```
|
|
|
|
### Refactor categories (Fowler)
|
|
```
|
|
1. Composing methods
|
|
2. Moving features between objects
|
|
3. Organizing data
|
|
4. Simplifying conditional expressions
|
|
5. Making method calls simpler
|
|
6. Dealing with generalization
|
|
7. Big refactorings
|
|
```
|
|
|
|
→ refactoring.com 무료.
|
|
|
|
## 🤔 의사결정 기준
|
|
| 상황 | 추천 |
|
|
|---|---|
|
|
| 매 PR | Boy scout (작은 cleanup) |
|
|
| Code review 발견 | 즉시 또는 follow-up issue |
|
|
| Hot spot (자주 변경) | 우선 refactor |
|
|
| 큰 architecture | Strangler fig + parallel change |
|
|
| Library version | Phased migration |
|
|
| Critical path | Test 먼저 |
|
|
|
|
## ❌ 안티패턴
|
|
- **Big bang refactor**: 깨짐 위험.
|
|
- **Test 없는 refactor**: behavior 변경 모름.
|
|
- **Refactor + new feature 같은 PR**: review 어려움.
|
|
- **Style only refactor**: lint / formatter 가 처리.
|
|
- **모든 코드 refactor 시도**: focus 잃음.
|
|
- **이해 못 한 코드 refactor**: bug 가능성.
|
|
- **Refactor 미공지 (대규모)**: surprise.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- 작은 reversible step.
|
|
- IDE refactoring 도구 활용.
|
|
- Test first 안전.
|
|
- Parallel change / strangler 큰 변경.
|
|
|
|
## 🔗 관련 문서
|
|
- [[Quality_Tech_Debt]]
|
|
- [[Productivity_Code_Review]]
|
|
- [[Testing_Test_Pyramid]]
|