--- 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 |