Files
2nd/10_Wiki/Topics/Coding/Arch_Strangler_Fig.md
T
2026-05-09 22:47:42 +09:00

7.1 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
arch-strangler-fig Strangler Fig — legacy 점진 교체 Coding draft B conceptual 2026-05-09 2026-05-09
architecture
migration
legacy
vibe-coding
language applicable_to
any
Architecture
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)

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)

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

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)

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)

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

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

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

// 옛 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 가 같은 결과 가 검증.
# 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% 가 큰 비.

🔗 관련 문서