🏗️ 아키텍처

Strangler Fig Pattern

레거시 시스템을 점진적으로 교체하는 패턴

📖 상세 설명

Strangler Fig Pattern은 레거시 시스템을 점진적으로 새 시스템으로 교체하는 마이그레이션 전략입니다. 호주의 교살무화과(Strangler Fig) 나무가 숙주 나무를 천천히 감싸 결국 대체하는 것에서 이름을 따왔습니다. Martin Fowler가 2004년 소개한 이후 모놀리스에서 마이크로서비스로의 전환에 널리 사용됩니다.

핵심 원리:

  • 점진적 교체: 전체 재작성(Big Bang) 대신 기능 단위로 교체
  • Facade 계층: 요청을 레거시 또는 새 시스템으로 라우팅
  • 병행 운영: 두 시스템이 공존하며 점진적으로 전환
  • 점진적 폐기: 모든 기능 이전 후 레거시 시스템 제거

구현 단계:

  1. Transform: 새 시스템에서 대체 기능 개발
  2. Coexist: Facade/Router로 트래픽 분기, 병행 운영
  3. Eliminate: 검증 후 레거시 기능 제거

Facade 구현 방식:

  • API Gateway: Kong, AWS API Gateway로 라우팅 규칙 정의
  • Reverse Proxy: Nginx, HAProxy로 URL 패턴별 분기
  • Service Mesh: Istio VirtualService로 트래픽 분할
  • Feature Flag: 사용자별, 비율별 새 시스템 노출

장점: 위험 분산(한 번에 조금씩), 롤백 용이, 비즈니스 연속성 유지, 팀 학습 곡선 완화. 단점: 두 시스템 동시 유지 비용, 복잡한 라우팅 로직, 장기 프로젝트화 위험.

💻 코드 예제

Nginx Reverse Proxy로 라우팅

# nginx.conf - Strangler Facade
upstream legacy_monolith {
    server legacy-app:8080;
}

upstream new_order_service {
    server order-service:8080;
}

upstream new_user_service {
    server user-service:8080;
}

server {
    listen 80;
    server_name api.example.com;

    # 새 마이크로서비스로 이전된 기능
    location /api/v2/orders {
        proxy_pass http://new_order_service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /api/v2/users {
        proxy_pass http://new_user_service;
        proxy_set_header Host $host;
    }

    # 아직 이전되지 않은 기능 → 레거시로 전달
    location /api/ {
        proxy_pass http://legacy_monolith;
        proxy_set_header Host $host;
    }

    # 단계적 전환: 10% 트래픽만 새 시스템으로
    location /api/v2/products {
        # 난수 기반 트래픽 분할
        set $backend legacy_monolith;
        if ($request_id ~* "[0-9]$") {
            set $backend new_product_service;
        }
        proxy_pass http://$backend;
    }
}

Node.js Express Router Facade

// strangler-facade.js
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';

const app = express();

// Feature Flag 서비스 (LaunchDarkly, Unleash 등)
const featureFlags = {
  'new-order-service': true,
  'new-user-service': true,
  'new-product-service': false, // 아직 이전 중
};

// 레거시 모놀리스
const legacyProxy = createProxyMiddleware({
  target: 'http://legacy-monolith:8080',
  changeOrigin: true,
});

// 새 마이크로서비스들
const orderServiceProxy = createProxyMiddleware({
  target: 'http://order-service:8080',
  changeOrigin: true,
  pathRewrite: { '^/api/orders': '/orders' },
});

const userServiceProxy = createProxyMiddleware({
  target: 'http://user-service:8080',
  changeOrigin: true,
});

// Strangler Router 미들웨어
function stranglerRouter(featureFlag, newProxy) {
  return (req, res, next) => {
    if (featureFlags[featureFlag]) {
      // 새 서비스로 라우팅
      console.log(`[Strangler] Routing to new service: ${featureFlag}`);
      return newProxy(req, res, next);
    }
    // 레거시로 폴백
    console.log(`[Strangler] Fallback to legacy: ${featureFlag}`);
    return legacyProxy(req, res, next);
  };
}

// 라우팅 규칙
app.use('/api/orders', stranglerRouter('new-order-service', orderServiceProxy));
app.use('/api/users', stranglerRouter('new-user-service', userServiceProxy));

// 이전되지 않은 모든 요청 → 레거시
app.use('/api', legacyProxy);

// 점진적 롤아웃 (퍼센트 기반)
function canaryRouter(percentage, newProxy) {
  return (req, res, next) => {
    const userId = req.headers['x-user-id'] || req.ip;
    const hash = hashCode(userId) % 100;

    if (hash < percentage) {
      return newProxy(req, res, next);
    }
    return legacyProxy(req, res, next);
  };
}

// 20% 트래픽만 새 상품 서비스로
app.use('/api/products', canaryRouter(20, productServiceProxy));

app.listen(3000);

Istio VirtualService로 트래픽 분할

# istio-strangler.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: api-strangler
spec:
  hosts:
    - api.example.com
  http:
    # 완전히 이전된 기능
    - match:
        - uri:
            prefix: /api/orders
      route:
        - destination:
            host: order-service
            port:
              number: 8080

    # 점진적 이전 중 (80% 레거시, 20% 새 서비스)
    - match:
        - uri:
            prefix: /api/products
      route:
        - destination:
            host: legacy-monolith
            port:
              number: 8080
          weight: 80
        - destination:
            host: product-service
            port:
              number: 8080
          weight: 20

    # 특정 헤더로 새 서비스 테스트
    - match:
        - headers:
            x-canary:
              exact: "true"
          uri:
            prefix: /api/inventory
      route:
        - destination:
            host: inventory-service

    # 나머지 → 레거시
    - route:
        - destination:
            host: legacy-monolith
            port:
              number: 8080

🗣️ 실무 대화 예시

마이그레이션 계획 회의

CTO: "10년 된 모놀리스를 마이크로서비스로 전환해야 하는데, Big Bang 리라이트는 리스크가 너무 커요."

아키텍트: "Strangler Fig Pattern으로 접근하죠. API Gateway를 앞에 두고, 기능별로 하나씩 새 서비스로 이전해요. 주문 도메인부터 시작해서 결제, 사용자 순으로요."

CTO: "기존 서비스는요?"

아키텍트: "그대로 운영하면서 점진적으로 트래픽을 이전해요. 문제 생기면 즉시 레거시로 롤백할 수 있고요. 1년 정도면 핵심 기능 이전 가능합니다."

기술 면접

면접관: "Strangler Fig Pattern의 단점은 뭐가 있나요?"

지원자: "첫째, 두 시스템을 동시에 운영해야 해서 인프라 비용과 운영 복잡도가 증가해요. 둘째, 데이터 일관성 유지가 어려워요. 레거시와 새 시스템이 같은 데이터를 참조할 때 동기화 문제가 생깁니다. 셋째, 프로젝트가 장기화되면 '영원한 마이그레이션' 상태에 빠질 수 있어요."

면접관: "데이터 동기화는 어떻게 해결하나요?"

지원자: "CDC(Change Data Capture)로 실시간 동기화하거나, 이벤트 소싱으로 양쪽에 이벤트를 발행하는 방법이 있어요. 또는 특정 기능 이전 시 관련 데이터도 함께 이전해서 의존성을 끊는 방법도 있습니다."

진행 상황 공유

PM: "주문 서비스 이전 현황이 어떻게 돼요?"

개발자: "지금 30% 트래픽을 새 서비스로 보내고 있어요. 에러율 0.1% 미만이고, 응답 시간도 레거시보다 40% 빨라요. 이번 주에 50%로 올리고, 다음 주에 100% 전환 예정이에요."

PM: "레거시 코드는요?"

개발자: "100% 전환 후 2주 모니터링하고, 문제 없으면 레거시 주문 모듈 코드를 제거할 계획이에요. 그래야 진짜 '교살'이 완료되는 거죠."

⚠️ 주의사항

🔗 관련 용어

Microservices API Gateway Feature Flag CDC Canary Release Domain-Driven Design

📚 더 배우기