f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
259 lines
8.4 KiB
Markdown
259 lines
8.4 KiB
Markdown
---
|
|
id: wiki-2026-0508-breaking-dependencies
|
|
title: Breaking Dependencies
|
|
category: 10_Wiki/Topics
|
|
status: verified
|
|
canonical_id: self
|
|
aliases: [Decoupling, Dependency Breaking, Refactoring Legacy]
|
|
duplicate_of: none
|
|
source_trust_level: A
|
|
confidence_score: 0.9
|
|
verification_status: applied
|
|
tags: [refactoring, architecture, legacy-code, testing]
|
|
raw_sources: []
|
|
last_reinforced: 2026-05-10
|
|
github_commit: pending
|
|
tech_stack:
|
|
language: Java / TypeScript / Python
|
|
framework: language-agnostic
|
|
---
|
|
|
|
# Breaking Dependencies
|
|
|
|
## 매 한 줄
|
|
> **"매 untestable code 를 매 test 가능하게 만드는 첫 단계 = 매 hard dependency 를 매 break."**. Michael Feathers 의 *Working Effectively with Legacy Code* (2004) 의 24 가지 dependency-breaking technique 가 매 canonical reference. 2026 현재 modern DI / 매 hexagonal architecture 가 매 prevention layer, but 매 legacy 에는 여전히 매 Sprout Method / Extract Interface / Adapter 가 핵심.
|
|
|
|
## 매 핵심
|
|
|
|
### 매 dependency types
|
|
- **Construction dependency**: 매 `new SomeService()` hardcoded.
|
|
- **Static dependency**: 매 `Date.now()`, `Math.random()`, `Singleton.getInstance()`.
|
|
- **Hidden dependency**: 매 method 안에서 file / DB / network 직접 호출.
|
|
- **Temporal coupling**: 매 method A → B 순서 강요.
|
|
- **Inheritance dependency**: 매 base class 가 매 hard-to-test 행위 가짐.
|
|
|
|
### 매 Feathers 의 핵심 24 technique 중 top 8
|
|
1. **Sprout Method**: 매 new behavior 를 매 새 method 로 분리해 매 test 가능하게.
|
|
2. **Sprout Class**: 매 new behavior 를 매 new class 로.
|
|
3. **Extract Interface**: 매 concrete dep → 매 interface → mock.
|
|
4. **Extract and Override Call**: 매 hard call 을 매 protected method 로 → subclass 에서 override.
|
|
5. **Subclass and Override Method**: 매 testing subclass.
|
|
6. **Adapt Parameter**: 매 untestable arg → 매 wrapper interface.
|
|
7. **Break Out Method Object**: 매 거대 method → 매 클래스.
|
|
8. **Introduce Instance Delegator**: 매 static → 매 instance method 로.
|
|
|
|
### 매 응용
|
|
1. Legacy Java/C# enterprise codebase 의 unit test 도입.
|
|
2. Module 간 cyclic dependency 끊기.
|
|
3. 매 third-party SDK 의 hard wiring 제거.
|
|
4. Microservice extraction 전 prep.
|
|
5. 매 monolith 의 plugin 화.
|
|
|
|
## 💻 패턴
|
|
|
|
### Extract Interface (Java)
|
|
```java
|
|
// BEFORE — hard wired
|
|
public class OrderService {
|
|
public Receipt placeOrder(Order o) {
|
|
EmailSender sender = new EmailSender(); // hard dep
|
|
sender.send(o.customerEmail, "...");
|
|
return Receipt.from(o);
|
|
}
|
|
}
|
|
|
|
// AFTER — Extract Interface + DI
|
|
public interface Notifier {
|
|
void send(String to, String body);
|
|
}
|
|
|
|
public class EmailSender implements Notifier { /* impl */ }
|
|
|
|
public class OrderService {
|
|
private final Notifier notifier;
|
|
public OrderService(Notifier notifier) { this.notifier = notifier; }
|
|
public Receipt placeOrder(Order o) {
|
|
notifier.send(o.customerEmail, "...");
|
|
return Receipt.from(o);
|
|
}
|
|
}
|
|
|
|
// Test
|
|
@Test void placesOrderAndNotifies() {
|
|
var fake = mock(Notifier.class);
|
|
var svc = new OrderService(fake);
|
|
svc.placeOrder(testOrder());
|
|
verify(fake).send(eq("a@b.com"), anyString());
|
|
}
|
|
```
|
|
|
|
### Sprout Method (TypeScript)
|
|
```ts
|
|
// BEFORE — 매 huge method, 매 add new logic 못 testable
|
|
class Invoice {
|
|
process(): void {
|
|
// 200 lines 의 legacy
|
|
// 매 new logic 추가하면 매 테스트 X
|
|
}
|
|
}
|
|
|
|
// AFTER — sprout 로 새 행동만 분리
|
|
class Invoice {
|
|
process(): void {
|
|
// 200 lines 의 legacy (그대로)
|
|
this.applyLoyaltyDiscount(); // 매 sprouted
|
|
}
|
|
|
|
applyLoyaltyDiscount(): number { // 매 testable in isolation
|
|
if (this.customer.tier === 'gold') return this.total * 0.1;
|
|
if (this.customer.tier === 'silver') return this.total * 0.05;
|
|
return 0;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Extract and Override Call (Python)
|
|
```python
|
|
# BEFORE
|
|
class Job:
|
|
def run(self):
|
|
now = datetime.utcnow() # 매 hard
|
|
if now.weekday() >= 5: return
|
|
self.do_work()
|
|
|
|
# AFTER
|
|
class Job:
|
|
def run(self):
|
|
if self._now().weekday() >= 5: return
|
|
self.do_work()
|
|
|
|
def _now(self) -> datetime: # 매 protected — override in test
|
|
return datetime.utcnow()
|
|
|
|
class TestJob(Job):
|
|
def __init__(self, fixed_dt): self.fixed_dt = fixed_dt
|
|
def _now(self): return self.fixed_dt
|
|
|
|
# Test
|
|
def test_skips_weekend():
|
|
j = TestJob(datetime(2026, 5, 9)) # 매 Saturday
|
|
j.do_work = MagicMock()
|
|
j.run()
|
|
j.do_work.assert_not_called()
|
|
```
|
|
|
|
### Adapt Parameter (Java)
|
|
```java
|
|
// BEFORE — HttpServletRequest hard to construct in test
|
|
public Result handle(HttpServletRequest req) {
|
|
String userId = req.getHeader("X-User-Id");
|
|
return repo.find(userId);
|
|
}
|
|
|
|
// AFTER — adapter interface
|
|
public interface RequestAdapter {
|
|
String header(String name);
|
|
}
|
|
|
|
public Result handle(RequestAdapter req) {
|
|
return repo.find(req.header("X-User-Id"));
|
|
}
|
|
|
|
// Production wraps; test = trivial map
|
|
class ServletRequestAdapter implements RequestAdapter {
|
|
private final HttpServletRequest delegate;
|
|
public String header(String n) { return delegate.getHeader(n); }
|
|
}
|
|
```
|
|
|
|
### Subclass and Override (Python — break DB)
|
|
```python
|
|
class ReportGenerator:
|
|
def fetch(self) -> list[dict]:
|
|
return db.query("SELECT * FROM orders") # 매 hard
|
|
|
|
def render(self) -> str:
|
|
rows = self.fetch()
|
|
return "\n".join(f"{r['id']}: {r['total']}" for r in rows)
|
|
|
|
class TestableReport(ReportGenerator):
|
|
def __init__(self, fake_rows): self.fake_rows = fake_rows
|
|
def fetch(self): return self.fake_rows
|
|
|
|
def test_renders():
|
|
r = TestableReport([{"id": 1, "total": 100}])
|
|
assert r.render() == "1: 100"
|
|
```
|
|
|
|
### Wrap Method (legacy 호환 유지)
|
|
```java
|
|
// 매 old API 깨면 안 됨 → 매 wrap
|
|
public void payInvoice(Invoice i) {
|
|
audit.log(i); // 매 new behavior
|
|
payInvoiceLegacy(i); // 매 unchanged
|
|
}
|
|
|
|
private void payInvoiceLegacy(Invoice i) { /* 매 untouched */ }
|
|
```
|
|
|
|
### Seam mapping checklist
|
|
```
|
|
1. List 매 untestable thing in target method
|
|
- Static call? → Introduce Instance Delegator / Replace Function with Function Object
|
|
- new? → Extract Interface + inject
|
|
- Time/random? → Extract & Override Call
|
|
- File/DB/HTTP? → Repository / Adapter
|
|
2. Pick 1 dep / day. 매 small step + commit.
|
|
3. 매 Test 먼저 (characterization test)
|
|
- 매 current behavior 를 lock down
|
|
4. Refactor under green tests.
|
|
```
|
|
|
|
### Hexagonal post-state (prevention)
|
|
```
|
|
[Domain Core] ← [Port (interface)] ← [Adapter (DB / HTTP / FS)]
|
|
매 NO direct import 매 from core to adapter
|
|
매 Test = stub adapter against port
|
|
```
|
|
|
|
## 매 결정 기준
|
|
| 상황 | Technique |
|
|
|---|---|
|
|
| 매 add new feature 매 large method | Sprout Method |
|
|
| 매 hard dep on concrete class | Extract Interface + DI |
|
|
| 매 static / time / random | Extract and Override Call |
|
|
| 매 framework type 의 arg | Adapt Parameter |
|
|
| 매 file / DB / network | Repository / Port-Adapter |
|
|
| 매 huge method 의 internal logic | Break Out Method Object |
|
|
| 매 base class behavior bad | Subclass and Override |
|
|
|
|
**기본값**: characterization test 먼저 → 매 minimal mechanical refactor (compiler-driven) → 매 small commit.
|
|
|
|
## 🔗 Graph
|
|
- 부모: [[Refactoring_Best_Practices|Refactoring]]
|
|
- 변형: [[Sprout-Method]]
|
|
- 응용: [[Test-Driven-Development]] · [[Hexagonal-Architecture]]
|
|
- Adjacent: [[Dependency_Injection_(DI)|Dependency-Injection]] · [[SOLID-Principles]] · [[Mocking]]
|
|
|
|
## 🤖 LLM 활용
|
|
**언제**: legacy code unit test 도입, untestable method 마주침, monolith 분해 직전 prep, third-party SDK lock-in 제거.
|
|
**언제 X**: 매 already clean architecture (over-refactor 금지), 매 throwaway prototype.
|
|
|
|
## ❌ 안티패턴
|
|
- **Big-bang rewrite**: 매 일부 break → 매 전체 unstable.
|
|
- **Test 없이 refactor**: 매 behavior 변경 모름 → 매 silent regression.
|
|
- **너무 많은 mock**: 매 test 가 매 implementation 에 결합.
|
|
- **Interface 의 1 implementation 영구**: 매 YAGNI — 매 두 번째 user 생기면 그때 추출.
|
|
- **God adapter**: 매 1 interface 에 30 method.
|
|
- **Refactor + feature 동시 commit**: 매 review 불가.
|
|
|
|
## 🧪 검증 / 중복
|
|
- Verified (Feathers "Working Effectively with Legacy Code" 2004, Fowler "Refactoring" 2nd ed).
|
|
- 신뢰도 A.
|
|
|
|
## 🕓 Changelog
|
|
| 날짜 | 변경 |
|
|
|---|---|
|
|
| 2026-05-08 | Phase 1 |
|
|
| 2026-05-10 | Manual cleanup — Breaking Dependencies with Feathers techniques |
|