Files
2nd/10_Wiki/Topics/Architecture/Sprout & Wrap Techniques (스프라우트 & 랩 기법).md
T
2026-05-10 22:08:15 +09:00

196 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: wiki-2026-0508-sprout-wrap-techniques-스프라우트-랩-기
title: "Sprout & Wrap Techniques (스프라우트 & 랩 기법)"
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [Sprout and Wrap, Wrap Method, Wrap Class, 스프라우트 랩]
duplicate_of: none
source_trust_level: A
confidence_score: 0.9
verification_status: applied
tags: [refactoring, legacy-code, michael-feathers]
raw_sources: []
last_reinforced: 2026-05-10
github_commit: pending
tech_stack:
language: java
framework: junit
---
# Sprout & Wrap Techniques (스프라우트 & 랩 기법)
## 매 한 줄
> **"매 legacy code 의 직접 수정 X — 매 sprout 의 옆에 새 method/class 의 추가, 매 wrap 의 기존 caller 의 둘러쌈"**. Michael Feathers, *Working Effectively with Legacy Code* (2004) 의 4 paired 기법: Sprout Method, Sprout Class, Wrap Method, Wrap Class.
## 매 핵심
### 매 4 techniques
- **Sprout Method**: 매 새 method 의 옆에 grow, 매 caller 의 1줄 add.
- **Sprout Class**: 매 새 class 의 grow, 매 caller 의 instantiate + delegate.
- **Wrap Method**: 매 기존 method rename → 매 same name 의 new method 의 둘러쌈 (before/after logic).
- **Wrap Class**: 매 decorator/proxy 의 wrap (cross-cutting: logging, auth).
### 매 Sprout vs Wrap
- **Sprout**: 매 add new logic to existing operation.
- **Wrap**: 매 add cross-cutting (before/after) to existing operation 의 callers.
### 매 응용
1. Add validation before legacy save.
2. Add audit logging across all calls.
3. Add caching layer transparently.
4. Add feature flag toggle.
## 💻 패턴
### Sprout Method
```java
public void process(Order o) {
// legacy untouched
repo.save(o);
notifyCustomer(o); // sprouted
}
void notifyCustomer(Order o) { // tested
email.send(o.customer(), template(o));
}
```
### Sprout Class — when sprout method 의 not enough
```java
// Original legacy class — touch minimally
public class OrderProcessor {
public void process(Order o) {
repo.save(o);
new OrderNotifier(email, sms).notifyAll(o); // sprouted class
}
}
// New, fully tested
public class OrderNotifier {
private final EmailService email;
private final SmsService sms;
public OrderNotifier(EmailService e, SmsService s) { this.email = e; this.sms = s; }
public void notifyAll(Order o) {
email.send(o.customer().email(), "thanks");
if (o.customer().smsOptIn()) sms.send(o.customer().phone(), "order placed");
}
}
```
### Wrap Method — rename + intercept
```java
public class Pay {
// Step 1: rename original to *Internal
private void payInternal(Order o) {
gateway.charge(o.amount());
}
// Step 2: new method with same original name
public void pay(Order o) {
audit.log("pay.start", o.id()); // before
payInternal(o); // delegate
audit.log("pay.end", o.id()); // after
}
}
```
### Wrap Class — Decorator
```java
public interface PaymentGateway { void charge(long amount); }
public class StripeGateway implements PaymentGateway { /* legacy untouched */ }
public class LoggingGateway implements PaymentGateway {
private final PaymentGateway inner;
private final Logger log;
public LoggingGateway(PaymentGateway inner, Logger log) {
this.inner = inner; this.log = log;
}
@Override public void charge(long amount) {
log.info("charge.start amount={}", amount);
try { inner.charge(amount); }
finally { log.info("charge.end"); }
}
}
// Wire-up: clients see PaymentGateway, get wrapped version
PaymentGateway gw = new LoggingGateway(new StripeGateway(), log);
```
### Wrap Class for caching
```java
public class CachingRepo implements OrderRepository {
private final OrderRepository inner;
private final Map<Long, Order> cache = new ConcurrentHashMap<>();
public CachingRepo(OrderRepository inner) { this.inner = inner; }
public Order findById(long id) {
return cache.computeIfAbsent(id, inner::findById);
}
public Order save(Order o) {
var saved = inner.save(o);
cache.put(saved.id(), saved);
return saved;
}
}
```
### Python — wrap with closure
```python
def wrap_with_audit(fn, audit):
def wrapped(*a, **kw):
audit.log(f"{fn.__name__}.start")
try: return fn(*a, **kw)
finally: audit.log(f"{fn.__name__}.end")
return wrapped
pay = wrap_with_audit(legacy_pay, audit)
```
### Sprout 의 test (TDD first)
```java
@Test
void notifyCustomer_priorityOrder_sendsPriorityTemplate() {
var o = new Order(customer, true);
new OrderProcessor(repo, emailMock, smsMock).notifyCustomer(o);
verify(emailMock).send(customer.email(), "priority-thanks");
}
```
## 매 결정 기준
| 상황 | Approach |
|---|---|
| Add small new logic to legacy method | **Sprout Method** |
| New logic 의 large/independent | **Sprout Class** |
| Add before/after to specific method | **Wrap Method** |
| Cross-cutting (logging, cache, auth) across many callers | **Wrap Class** (decorator) |
| Legacy call sites 의 모두 update 의 가능 | Direct refactor 의 OK |
**기본값**: 매 legacy add → Sprout. 매 cross-cutting → Wrap Class.
## 🔗 Graph
- 부모: [[Working Effectively with Legacy Code]] · [[Refactoring]]
- 변형: [[Sprout Method (스프라우트 메서드)]] · [[Decorator Pattern]]
- 응용: [[Legacy Code Modernization]] · [[Strangler Fig Pattern]]
- Adjacent: [[Adapter Pattern]] · [[Proxy Pattern]] · [[Aspect-Oriented Programming]]
## 🤖 LLM 활용
**언제**: 매 untested legacy code 의 modification 필요; 매 cross-cutting concern 의 add.
**언제 X**: 매 codebase 의 well-tested — 매 직접 refactor 의 cleaner.
## ❌ 안티패턴
- **Wrap without rename**: 매 infinite recursion.
- **Sprout class 의 1-method-only forever**: 매 결국 dead weight — 매 collapse 의 검토.
- **Wrap class 의 not used by all callers**: 매 inconsistent behavior — DI 의 enforce.
- **No tests on sprouted/wrapped code**: 매 entire technique 의 point 의 lost.
## 🧪 검증 / 중복
- Verified (Feathers, *WELC* ch.67, 2004; Fowler refactoring catalog).
- 신뢰도 A.
## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — full Sprout & Wrap quartet with Java/Python patterns |