--- 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; } 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]]