모놀리스는 하루아침에 죽지 않는다 — Strangler Fig로 트래픽 경계와 데이터 동기화를 설계하는 법
레거시 시스템 마이그레이션 미팅에서 누군가 "그냥 처음부터 새로 짜면 안 돼요?"라고 물으면, 저는 속으로 한숨을 쉽니다. 빅뱅 재작성이 실패하는 이야기는 업계에 셀 수 없이 많고, 그 실패는 대부분 "다 완성되면 한 번에 바꾸자"는 순간에서 시작됩니다. 운영 중인 시스템을 멈출 수 없고, 팀은 새 코드와 레거시 버그를 동시에 고쳐야 하고, 어느 순간 두 버전 사이의 격차가 너무 커져 돌이킬 수 없게 됩니다.
Strangler Fig 패턴은 이 문제를 정직하게 직면합니다. 열대우림의 교살목처럼, 새 서비스가 모놀리스를 서서히 감싸고 기능 슬라이스 하나씩을 흡수해 나갑니다. Martin Fowler가 이름을 붙인 이 패턴의 핵심은 단순합니다 — 시스템 전체를 한 번에 바꾸려 하지 말고, 기능 경계를 식별해서 하나씩 이관하라.
그런데 막상 실무에서 이 패턴을 적용하다 보면 두 가지 지점에서 막힙니다. 첫째, 어디서 트래픽을 자를 것인가 — 경계를 어떻게 정의하고, 어떻게 점진적으로 전환할 것인가. 둘째, 데이터는 언제 분리할 것인가 — 코드 추출보다 훨씬 어렵고, 잘못하면 분산 모놀리스를 만들게 됩니다. 이 글은 그 두 가지에 집중합니다.
패턴의 실제 작동 방식 — 세 단계 루프
이론은 단순합니다. 실제로 돌아가는 사이클을 다이어그램으로 먼저 보겠습니다.
핵심은 이 루프가 반복된다는 점입니다. 한 슬라이스가 완전히 이관되면, 모놀리스에서 해당 코드를 즉시 삭제하고 다음 슬라이스로 넘어갑니다. 삭제를 미루면 나중에 "혹시 몰라서 남겨뒀던 코드"가 혼란을 만듭니다.
슬라이스 선택 기준
DDD의 Bounded Context와 Event Storming을 통해 이관 후보를 고릅니다. 좋은 첫 번째 슬라이스는 다음 조건을 만족합니다.
| 기준 | 이유 |
|---|---|
| 명확한 입출력 경계 | 모놀리스와의 결합점을 최소화해서 이관 범위가 커지는 걸 방지 |
| 낮은 결합도 | 다른 도메인에 동기 호출로 의존하는 게 적을수록 좋음 |
| 높은 비즈니스 가치 | 조기 성과를 만들어 이해관계자 신뢰를 얻음 |
| 독립적인 데이터 | 공유 테이블이 없는 도메인이 첫 단계에 적합 |
notifications나 user-management 같은 도메인이 자주 첫 이관 대상이 되는 이유가 여기 있습니다. order-processing처럼 여러 도메인과 얽혀 있는 핵심 도메인은 나중 단계로 미룹니다.
트래픽 전환 경계 — Façade가 전부를 결정합니다
Façade의 역할
API 게이트웨이가 모놀리스 앞에 자리 잡으면, 모든 요청이 이 게이트웨이를 거칩니다. 이관이 완료된 기능은 새 서비스로, 아직 이관 중인 기능은 모놀리스로 통과시킵니다.
이 Façade를 어떻게 구성하느냐에 따라 이관 속도와 안전성이 결정됩니다.
Kong으로 라우팅 규칙 구성하기
아래는 개념적 예시입니다. Kong의 Declarative Config(deck) 포맷을 기반으로 했습니다.
# kong.yaml (개념적 예시 - deck 포맷)
services:
- name: user-service
url: http://user-service:8080
routes:
- name: user-routes
paths:
- /api/users
- /api/auth
# user-management 이관 완료 → 새 서비스로 전달
- name: monolith
url: http://monolith:8080
routes:
- name: monolith-routes
paths:
- /api/orders
- /api/products
# 아직 이관 전 도메인 → 모놀리스로 통과
plugins:
- name: request-transformer
service: user-service
config:
add:
headers:
- "X-Migrated-Service:true"캐너리 배포로 단계적 전환
트래픽을 한 번에 전환하지 않고 여러 단계로 나눠 이동시킵니다. 예를 들어 조직의 위험 허용 수준에 따라 1% → 10% → 50% → 100%처럼 잡을 수도 있고, 훨씬 보수적으로 0.1%부터 시작할 수도 있습니다. 정답은 없고, 팀이 실패를 얼마나 빨리 감지하고 되돌릴 수 있느냐로 결정됩니다. AWS ALB라면 가중치 대상 그룹으로, Istio라면 VirtualService로 비율을 제어할 수 있습니다.
# Istio VirtualService 예시 (개념적)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: order-processing
spec:
hosts:
- order-processing.internal
http:
- route:
- destination:
host: order-service # 새 마이크로서비스
weight: 25
- destination:
host: monolith
weight: 75캐너리 단계에서 가장 중요한 건 롤백 조건을 미리 정의하는 겁니다. "에러율이 1%를 넘으면 자동으로 이전 상태로" 같은 SLO 기반 조건이 없으면, 문제가 생겼을 때 판단이 늦어집니다.
Feature Flag로 세밀한 제어 추가하기
LaunchDarkly나 Unleash 같은 피처 플래그 도구를 게이트웨이에 통합하면 특정 사용자 세그먼트 단위로 트래픽을 전환할 수 있습니다. 예를 들어 '베타 사용자 그룹만 새 주문 서비스로'처럼요. 문제가 생기면 플래그 하나로 즉시 롤백됩니다. 코드 배포 없이.
솔직히 처음에는 피처 플래그가 과하다고 생각했는데, 캐너리 배포만으로는 특정 계정에서 재현되는 버그를 격리하기가 어렵다는 걸 경험하고 나서 생각이 바뀌었습니다.
데이터 동기화 — 진짜 어려운 부분
코드는 추출하면 끝이지만, 데이터는 다릅니다. 서비스를 분리했는데 DB를 공유하고 있다면, 논리적으로는 마이크로서비스지만 실제로는 분산 모놀리스입니다.
데이터베이스 분리는 세 단계로 진행됩니다.
Stage 1: 공유 DB — 빠르게 시작하기
새 서비스와 모놀리스가 같은 DB를 사용합니다. 코드는 분리됐지만 데이터는 아직 하나입니다. 초기에 이 상태가 필요한 이유는 DB 분리 없이 코드 추출만 먼저 검증할 수 있기 때문입니다.
이 단계는 과도기입니다. 여기서 오래 머물면 강결합이 그대로 유지됩니다. 새 서비스의 자체 DB 스키마 설계가 끝나고 쓰기 경로를 옮길 준비가 되면 Stage 2로 넘어갑니다.
Stage 2: 이중 쓰기 — 소유권 이전의 과도기
이중 쓰기는 임시 방편으로 시작됐다가 아무도 제거하지 못하는 핵심 인프라로 굳는 경우가 많습니다. "나중에 없앨 거야"라는 생각으로 시작한 동기화 레이어가 2년 후 아무도 손 못 대는 블랙박스가 되는 걸 본 적 있습니다. Stage 2 코드를 쓰기 전에 이 문장을 먼저 붙여두는 이유입니다.
이 단계에서는 새 서비스가 자체 DB에 쓰고, 같은 요청 처리 흐름 안에서 모놀리스 DB에도 함께 씁니다. 아래 예시는 별도의 동기화 컴포넌트를 두지 않고 인라인으로 두 DB를 호출하는 가장 단순한 형태입니다. 실제로는 두 write를 트랜잭셔널 아웃박스(outbox) 테이블 하나로 통합해서 원자성을 확보하는 편이 안전합니다.
# 이중 쓰기 - 인라인 방식 (개념적 예시, 프로덕션용 아님)
def create_order(order_data):
# 새 서비스 DB에 저장
new_db.orders.insert(order_data)
# 주의: 이 라인 직전에 프로세스가 크래시하면 새 DB에만 데이터가 존재하고
# 모놀리스 DB로의 동기화는 시도조차 되지 않습니다. try/except로는
# 예외만 잡을 수 있고 프로세스 종료는 잡을 수 없습니다. 원자성이
# 필요하면 아웃박스 패턴이나 트랜잭셔널 큐로 두 write를 하나의
# 로컬 트랜잭션에 담아야 합니다.
try:
legacy_db.orders.insert(transform_to_legacy(order_data))
except Exception as e:
sync_queue.enqueue({"action": "sync_order", "data": order_data})
logger.error(f"Legacy sync failed: {e}")Stage 2에서 Stage 3으로 넘어가는 판단 기준은 명확합니다. 모놀리스 쪽 쓰기 경로가 실질적으로 사라져서, 새 서비스가 유일한 쓰기 주체가 되는 시점입니다. 구체적으로는 (1) 모놀리스 코드에서 해당 도메인 테이블에 대한 write 경로가 모두 제거되었고, (2) 캐너리 100%가 새 서비스로 향하고, (3) 일정 기간 동안 모놀리스 DB에 대한 직접 write가 관찰되지 않을 때입니다. 이 상태가 되기 전에 CDC로 전환하면, 양쪽에서 쓰는 상태에서 이벤트 방향이 꼬여 일관성이 무너집니다.
Stage 3: CDC로 완전한 분리
Debezium은 PostgreSQL이나 MySQL의 트랜잭션 로그(WAL/binlog)를 감지해 변경 이벤트를 Kafka에 발행합니다. 모놀리스 코드를 한 줄도 건드리지 않고 데이터 변경을 감지할 수 있습니다.
// Debezium이 Kafka에 발행하는 이벤트 예시 (구조 예시, 필드값은 임의)
{
"before": null,
"after": {
"id": 12345,
"user_id": 789,
"status": "created",
"total": 59000,
"created_at": 1720000000000
},
"source": {
"version": "2.3.0.Final",
"connector": "postgresql",
"db": "monolith_db",
"table": "orders"
},
"op": "c"
}source.version은 실제로는 사용 중인 Debezium 릴리스 문자열이 들어갑니다(위 예시의 2.3.0.Final은 형식 예시일 뿐이며, 프로젝트에서 채택한 정확한 버전으로 대체됩니다). 새 서비스는 이 Kafka 이벤트를 구독해서 자체 DB를 업데이트합니다.
Debezium + Kafka 조합은 CDC 기반 데이터 동기화에서 가장 널리 채택되는 조합 중 하나이고, 참고 자료에 실린 Gunnar Morling의 Debezium + Kafka + MongoDB 발표가 이 접근법의 실전 사례를 잘 보여줍니다. 이 접근법의 핵심 장점은 최종 일관성(Eventual Consistency)을 명시적으로 받아들이는 대신, 모놀리스 코드를 건드리지 않는다는 것입니다.
실제 시나리오 — e-커머스 주문 시스템 분해
실제 대규모 이커머스 플랫폼에서 자주 볼 수 있는 시나리오를 예로 들겠습니다. API 게이트웨이의 라우팅 테이블에 각 도메인의 이관 상태를 명시적으로 관리합니다.
# 라우팅 테이블 스냅샷 (예시 - 2026-09-10 기준)
routing_rules:
- domain: user-management
status: migrated # 이관 완료
target: user-service:8080
canary_weight: 100
- domain: notifications
status: migrated # 이관 완료
target: notification-service:8080
canary_weight: 100
- domain: order-processing
status: in_progress # 현재 캐너리 단계
targets:
- service: order-service:8080
weight: 25
- service: monolith:8080
weight: 75
- domain: inventory
status: pending # 이관 예정
target: monolith:8080
canary_weight: 0order-processing이 25% 캐너리 단계일 때 이미 완료된 user-management와 notifications는 새 서비스로만 트래픽이 흐릅니다. 모놀리스에는 해당 코드가 이미 삭제된 상태입니다.
데이터 소유권은 하나여야 합니다
이 과정에서 가장 피해야 할 상황은 모놀리스와 새 서비스가 같은 테이블을 공동으로 쓰는 것입니다. 어느 시점부터 어떤 시스템이 '진실의 원천(Source of Truth)'인지 불분명해지면 일관성 보장이 불가능해집니다.
원칙은 명확합니다. 데이터를 쓰는 쪽(Owner)은 항상 하나여야 합니다. 캐너리 단계에서도 특정 요청이 새 서비스로 갔다면 그 데이터는 새 서비스 DB에 씁니다. 모놀리스로 간 요청은 모놀리스 DB에 씁니다. 두 DB 사이의 동기화는 CDC가 맡습니다.
트레이드오프 — 알면서 선택해야 합니다
| 항목 | 장점 | 주의사항 |
|---|---|---|
| 무중단 전환 | 운영 중 마이그레이션, 빅뱅 재작성 없음 | Façade 자체가 단일 장애점이 될 수 있음 |
| 점진적 위험 분산 | 한 슬라이스 실패가 전체에 영향 없음 | 두 시스템 동시 운영 비용 증가 |
| 즉각 롤백 | 피처 플래그나 게이트웨이 설정으로 즉시 복구 | 롤백 조건을 사전에 정의하지 않으면 판단이 늦어짐 |
| CDC 동기화 | 모놀리스 코드 변경 없이 데이터 감지 | 최종 일관성 — 실시간 일관성이 필요한 도메인엔 부적합 |
| 조직 학습 효과 | 초기 이관에서 운영 노하우 확보 | 팀 경계와 도메인 경계가 불일치하면 경계 유지가 어려움 |
실무에서 자주 보이는 실수
1. 모놀리스 코드 동결 실패 이관 중에도 모놀리스에 새 기능을 추가하면 이관 범위가 계속 늘어납니다. 새 기능은 반드시 새 서비스에만 추가해야 합니다. 이 규칙이 지켜지지 않으면 마이그레이션이 끝나지 않습니다.
2. 경계 설정 오류 서비스 경계를 잘못 그으면 이관 후에도 모놀리스에 대한 동기 호출 의존성이 남습니다. 이른바 분산 모놀리스입니다. 네트워크 호출이 추가된 것 외에 아무것도 나아지지 않은 상황이 됩니다. Event Storming으로 도메인 경계를 코드 레벨에서 먼저 정리한 후 물리적 서비스 분리를 진행하는 — 모놀리스 → 모듈형 모놀리스 → 마이크로서비스 — 2단계 경로가 이 리스크를 줄이는 방법입니다.
3. 관찰가능성 없이 첫 추출 시도 분산 추적(Jaeger, Zipkin), 메트릭(Prometheus + Grafana), 로그 집계가 갖춰지기 전에 첫 서비스를 배포하면, 문제가 생겼을 때 어디서 왜 그런지 알 수 없습니다. OpenTelemetry로 계측을 먼저 갖추는 게 순서입니다.
4. 매니지드 마이그레이션 서비스에 대한 잠금 AWS Refactor Spaces 같은 매니지드 도구는 Façade 구성을 빠르게 시작할 수 있게 해주지만, 특정 벤더 제품의 신규 제공 정책이 변경될 수 있으니(최근에도 유사한 사례가 있었습니다) 도구 채택 전에 반드시 해당 벤더의 최신 공식 공지를 확인하세요. 대안으로는 Application Load Balancer 가중치 라우팅이나 API Gateway를 직접 조합하는 방식이 있고, 이 경로는 벤더 종속성이 낮다는 장점이 있습니다.
마무리 — 경계와 타이밍, 두 축의 실수 지점
Strangler Fig 패턴이 매력적인 이유는 실패해도 되돌아갈 수 있기 때문입니다. 한 슬라이스를 이관했는데 잘못됐다면, 게이트웨이 설정을 바꿔 모놀리스로 되돌리면 됩니다. 빅뱅 재작성에서 실패하면 수개월의 작업이 날아가지만, 여기서 실패하면 그 슬라이스 하나만 롤백됩니다.
이 글은 트래픽 경계와 데이터 타이밍 두 축을 다뤘습니다. 그 두 축에서 팀이 가장 자주 넘어지는 지점을 교차 정리하며 닫습니다.
트래픽 경계에서 자주 실수하는 지점 게이트웨이 라우팅을 코드가 아니라 사람 머릿속에서 관리하기 시작하면, 어느 도메인이 어느 단계인지 아무도 확신하지 못하는 순간이 옵니다. 라우팅 테이블은 반드시 선언적으로, 리포지토리에 커밋된 형태로 관리해야 합니다. 그리고 캐너리 롤백 조건을 SLO로 미리 못 박아두지 않으면, 문제가 생겨도 "조금 더 지켜보자"는 판단이 반복되면서 롤백 타이밍을 놓칩니다.
데이터 타이밍에서 자주 실수하는 지점 Stage 2 이중 쓰기에 너무 오래 머무르는 것과, Stage 3 CDC로 너무 일찍 넘어가는 것이 양극단의 실수입니다. 전자는 동기화 레이어가 굳어져 지워지지 않는 결과를, 후자는 양쪽에서 쓰는 상태에서 이벤트가 꼬여 일관성이 무너지는 결과를 낳습니다. 넘어가는 시점은 "모놀리스 쪽 쓰기 경로가 실질적으로 사라졌는가"라는 단 하나의 기준으로 판단합니다.
두 축이 만나는 지점 가장 위험한 상황은 트래픽은 새 서비스로 이동시켰는데 데이터 소유권은 아직 모놀리스에 남아있는 경우입니다. 요청이 새 서비스로 흘러도 결국 모놀리스 DB에 써야 한다면 네트워크 홉만 하나 추가한 셈입니다. 트래픽 전환 계획과 데이터 소유권 이전 계획은 같은 캘린더에 그려져야 합니다.
참고 자료
- Strangler Fig Pattern — Azure Architecture Center
- Strangler Fig Pattern — AWS Prescriptive Guidance
- Strangler Fig Pattern for Modular Monolith Migration — Milan Jovanović
- Replacing Legacy Systems One Step at a Time with Data Streaming — Kai Waehner (2025)
- Strangler Fig Pattern with Event Streaming — Conduktor
- The Hidden Impediment of the Strangler Fig Pattern — Vahid Bakhtiary
- Embracing the Strangler Fig Pattern for Legacy Modernization — Thoughtworks
- Monolith to Microservices Migration: Strangler Fig, DDD, and Why Most Teams Get It Wrong — CloudRPS
- Designing the Façade: How API Gateways Make the Strangler Fig Pattern Work — ItsAVirus
- Dissecting Our Legacy: Strangler Fig with Debezium, Kafka & MongoDB — Gunnar Morling