[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
---
|
||||
id: arch-strangler-fig
|
||||
title: Strangler Fig — legacy 점진 교체
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [architecture, migration, legacy, vibe-coding]
|
||||
tech_stack: { language: "any", applicable_to: ["Architecture"] }
|
||||
applied_in: []
|
||||
aliases: [strangler fig, strangler pattern, legacy migration, branch by abstraction, rewrite, big bang]
|
||||
---
|
||||
|
||||
# Strangler Fig Pattern
|
||||
|
||||
> Legacy 를 한 번에 교체 — 거의 항상 실패. **Strangler fig: facade 뒤에 새 + 옛 공존, 한 endpoint 씩 옮김**. Martin Fowler 의 idea (열대 식물 비유).
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Big bang rewrite ≈ 망함.
|
||||
- Facade / proxy 가 routing.
|
||||
- 새 system 가 옛 을 점차 cover.
|
||||
- 옛 system 가 0% traffic = 종료.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### 일반 진행
|
||||
```
|
||||
Phase 0: Legacy monolith (100%)
|
||||
|
||||
Phase 1: Facade 추가
|
||||
Client → Facade → Legacy
|
||||
|
||||
Phase 2: New service 추가, 1 endpoint
|
||||
Client → Facade →
|
||||
/api/users → New
|
||||
others → Legacy
|
||||
|
||||
Phase 3: 점차 endpoint 이동
|
||||
Client → Facade →
|
||||
/api/users, /api/orders, /api/items → New
|
||||
others → Legacy
|
||||
|
||||
Phase 4: Legacy 0%, 종료.
|
||||
```
|
||||
|
||||
### Facade (NGINX)
|
||||
```nginx
|
||||
upstream legacy { server legacy:8080; }
|
||||
upstream new { server new:8080; }
|
||||
|
||||
server {
|
||||
location /api/users {
|
||||
proxy_pass http://new;
|
||||
}
|
||||
location / {
|
||||
proxy_pass http://legacy;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Facade (Hono)
|
||||
```ts
|
||||
import { Hono } from 'hono';
|
||||
const app = new Hono();
|
||||
|
||||
const NEW_PATHS = ['/api/users', '/api/orders'];
|
||||
|
||||
app.all('*', async (c) => {
|
||||
const path = c.req.path;
|
||||
const target = NEW_PATHS.some(p => path.startsWith(p)) ? 'new:8080' : 'legacy:8080';
|
||||
return fetch(`http://${target}${path}`, c.req.raw);
|
||||
});
|
||||
```
|
||||
|
||||
### 단계
|
||||
```
|
||||
1. Read 만 (write 는 legacy)
|
||||
2. Read + write 둘 다 (dual write — 검증)
|
||||
3. Read + write 만 new
|
||||
4. Legacy 종료
|
||||
```
|
||||
|
||||
### Dual write
|
||||
```ts
|
||||
async function createUser(data) {
|
||||
// 1. Legacy 가 source of truth
|
||||
const legacy = await legacyAPI.create(data);
|
||||
|
||||
// 2. New 도 (검증)
|
||||
try {
|
||||
await newAPI.create(data);
|
||||
} catch (e) {
|
||||
log.warn('new system write failed', e);
|
||||
}
|
||||
|
||||
return legacy;
|
||||
}
|
||||
```
|
||||
|
||||
→ 결과 비교 — 같으면 OK. Verify 후 reverse.
|
||||
|
||||
### Read-and-compare (shadow)
|
||||
```ts
|
||||
async function getUser(id) {
|
||||
const legacy = await legacyAPI.get(id);
|
||||
|
||||
// 검증 — async, 결과 안 사용
|
||||
asyncio.run(async () => {
|
||||
const newR = await newAPI.get(id);
|
||||
if (!deepEqual(legacy, newR)) {
|
||||
log.error('mismatch', { legacy, newR });
|
||||
}
|
||||
});
|
||||
|
||||
return legacy;
|
||||
}
|
||||
```
|
||||
|
||||
→ 1주 모니터링 → 차이 없으면 swap.
|
||||
|
||||
### Branch by abstraction (in-code)
|
||||
```ts
|
||||
interface UserRepo {
|
||||
get(id: string): Promise<User>;
|
||||
}
|
||||
|
||||
class LegacyUserRepo implements UserRepo {
|
||||
// 옛 코드
|
||||
}
|
||||
|
||||
class NewUserRepo implements UserRepo {
|
||||
// 새 코드
|
||||
}
|
||||
|
||||
// Feature flag
|
||||
const repo: UserRepo = flags.useNewRepo ? new NewUserRepo() : new LegacyUserRepo();
|
||||
```
|
||||
|
||||
→ Legacy 안에서 점진 교체.
|
||||
|
||||
### Database sync
|
||||
```
|
||||
Legacy DB ↔ New DB
|
||||
- CDC (Debezium) — legacy → new
|
||||
- Dual write — 둘 다
|
||||
- ETL — 매일
|
||||
|
||||
→ 둘 다 작동 시점 = 가장 risky.
|
||||
```
|
||||
|
||||
### Schema bridge
|
||||
```sql
|
||||
-- New view 가 legacy schema 모방
|
||||
CREATE VIEW legacy.users AS
|
||||
SELECT
|
||||
id::int as user_id,
|
||||
full_name as name,
|
||||
created_at::timestamp as created
|
||||
FROM new.users;
|
||||
```
|
||||
|
||||
→ Legacy app 가 그대로 query.
|
||||
|
||||
### Anti-corruption layer (ACL)
|
||||
```ts
|
||||
// Legacy 의 model 이상 — 새 system 가 영향 X
|
||||
class LegacyUserAdapter {
|
||||
fromLegacy(raw: any): User {
|
||||
return {
|
||||
id: raw.user_id,
|
||||
email: raw.email_address,
|
||||
// legacy 특이성 hide
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
→ Legacy 의 messy / weird 가 새 system 에 침투 X.
|
||||
|
||||
### Routing 전략
|
||||
```
|
||||
1. Path-based: /api/users/* → new
|
||||
2. Header-based: X-Use-New: 1 → new
|
||||
3. User-based: hash(user_id) % 100 < N → new
|
||||
4. Feature flag: per-request
|
||||
```
|
||||
|
||||
### Canary (점진 traffic)
|
||||
```
|
||||
Day 1: 1% → new
|
||||
Day 7: 10%
|
||||
Day 14: 50%
|
||||
Day 21: 100%
|
||||
|
||||
→ 매 단계 monitoring + rollback ready.
|
||||
```
|
||||
|
||||
### Rollback 가능
|
||||
```
|
||||
중요: 매 단계 rollback 가능 해야.
|
||||
- Dual write (data sync)
|
||||
- Feature flag (instant switch)
|
||||
- Backward compatible API
|
||||
```
|
||||
|
||||
### Migration script
|
||||
```ts
|
||||
// 옛 user → 새 schema (one-time)
|
||||
async function migrate() {
|
||||
for await (const u of legacyDB.users.stream()) {
|
||||
await newDB.users.insert(transform(u));
|
||||
}
|
||||
}
|
||||
|
||||
// Idempotent (다시 실행 OK)
|
||||
await newDB.users.upsert(transform(u));
|
||||
```
|
||||
|
||||
### Testing legacy
|
||||
```
|
||||
- Characterization tests (현재 동작 = test)
|
||||
- Snapshot test
|
||||
- Gold master (input → output)
|
||||
|
||||
→ 새 system 가 같은 결과 가 검증.
|
||||
```
|
||||
|
||||
```python
|
||||
# Approval test
|
||||
import pytest
|
||||
from approvaltests import verify
|
||||
|
||||
def test_user_serialize():
|
||||
u = legacy.serialize(sample_user)
|
||||
verify(u) # 첫 실행 = 저장. 변경 = 수동 승인.
|
||||
```
|
||||
|
||||
### Common pitfalls
|
||||
```
|
||||
1. New system 가 legacy 보다 못함 (성능, feature)
|
||||
2. Migration 가 1년 → 우선순위 변경 → 멈춤
|
||||
3. Dual write 의 race condition
|
||||
4. Legacy code 의 hidden behavior (timing, side effects)
|
||||
```
|
||||
|
||||
### "Last 10%" problem
|
||||
```
|
||||
처음 90% 빠름. 마지막 10% (특이 endpoint, edge case) 가 6 month+.
|
||||
|
||||
→ Plan 시 보수적. "끝" 가 큰 비.
|
||||
```
|
||||
|
||||
### Brownfield refactor (one-codebase)
|
||||
```
|
||||
Legacy code → 점차 모듈화.
|
||||
1. 상속 / coupling 끊기
|
||||
2. Interface 추출
|
||||
3. Test 추가
|
||||
4. 새 implementation 교체
|
||||
5. 옛 삭제
|
||||
|
||||
→ Big rewrite 안 됨. 작은 step.
|
||||
```
|
||||
|
||||
### Big bang rewrite
|
||||
```
|
||||
"새 versions 만들고 한 번에 교체!"
|
||||
|
||||
거의 항상:
|
||||
- Plan 의 2-5x 시간
|
||||
- New system 가 legacy 의 hidden feature 잃음
|
||||
- Stakeholder 신뢰 잃음
|
||||
- Cancelled
|
||||
|
||||
→ Strangler fig 가 실용적.
|
||||
```
|
||||
|
||||
→ Joel Spolsky "Things You Should Never Do" 참고.
|
||||
|
||||
### 정치 / 인적 관리
|
||||
```
|
||||
Legacy 의 owner 가 새 가 맘에 안 들 수.
|
||||
- Stakeholder buy-in
|
||||
- 진척 visibility (dashboard)
|
||||
- Quick win (1-2 endpoint 빠른 migrate)
|
||||
- 작은 milestone
|
||||
```
|
||||
|
||||
### Success story 패턴
|
||||
- Twitter: Ruby → Scala (years).
|
||||
- GitHub: Rails → 일부 Go services.
|
||||
- Slack: PHP → Hack → 점차.
|
||||
- Shopify: Rails monolith → modular Rails.
|
||||
|
||||
### 비용 예상
|
||||
```
|
||||
새 system: 6 month
|
||||
+ Migration: 1 year
|
||||
+ Validation / dual run: 6 month
|
||||
+ Cleanup: 3 month
|
||||
= 약 2-3 year (큰 system).
|
||||
|
||||
→ Realistic.
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| 큰 legacy | Strangler fig + facade |
|
||||
| 작은 legacy (몇 endpoint) | Big bang OK |
|
||||
| 데이터 다른 | CDC + dual write |
|
||||
| Schema 같음 | Branch by abstraction |
|
||||
| Risk 큰 | Shadow → A/B → 100% |
|
||||
| 시간 < 6 month | 작은 scope 만 |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Big bang rewrite**: 거의 망함.
|
||||
- **Facade 없이 dual stack**: client 가 둘 다 알아야.
|
||||
- **Rollback 안 됨**: 안전성 X.
|
||||
- **Migration 영원히**: 끝 가 plan.
|
||||
- **Test 없이 migrate**: bug 옮김.
|
||||
- **Performance regression 검증 X**: prod 에서 발견.
|
||||
- **One-shot migration script**: race condition.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- Strangler fig + facade = canonical.
|
||||
- ACL 가 legacy 의 mess 차단.
|
||||
- Dual write 가 verification 의 답.
|
||||
- 마지막 10% 가 큰 비.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Arch_Modular_Monolith]]
|
||||
- [[Backend_BFF_Pattern]]
|
||||
- [[Productivity_Migration_Runbook]]
|
||||
Reference in New Issue
Block a user