TL;DR

  • Dual Write는 DB와 브로커 등 서로 다른 시스템에 동시에 쓰기할 때 발생하는 원자성 문제다.
  • Transactional Outbox와 CDC는 가장 범용적인 해결책으로 단일 쓰기 지점을 만든다.
  • Saga, Event Sourcing, Idempotency 같은 패턴을 상황에 맞게 조합해야 한다.

1. 개념

Dual Write는 하나의 논리 작업에서 두 개 이상의 독립된 시스템에 동시에 쓰기를 시도할 때 발생하는 구조적 문제다.

2. 배경

마이크로서비스와 Database-per-Service 구조로 전환되며 DB 업데이트와 이벤트 발행이 분리되어 원자성을 보장하기 어려워졌다.

3. 이유

부분 실패, 크래시, 네트워크 파티션 같은 현실적인 장애가 발생하면 한쪽만 성공하는 불일치가 생기기 때문이다.

4. 특징

  • 동시 쓰기 자체가 근본 원인이라 애초에 단일 쓰기 지점을 만들어야 한다
  • Outbox, CDC, Saga, Event Sourcing 등 여러 패턴이 계층적으로 합성된다
  • 멱등성은 선택이 아닌 필수이며, 시스템 특성에 맞는 조합이 필요하다

5. 상세 내용

Dual Write 패턴

작성일: 2026-02-28 카테고리: Backend / Microservices / Distributed Systems 포함 내용: Dual Write, 이중 쓰기, Transactional Outbox, CDC, Change Data Capture, Debezium, Event Sourcing, Saga, CQRS, Listen to Yourself, 분산 트랜잭션, 2PC, 멱등성, Idempotency, fail-fast, Kafka, 마이크로서비스, 데이터 일관성


1. Dual Write란?

┌─────────────────────────────────────────────────────────────────┐
│              Dual Write = 이중 쓰기 문제                        │
│                                                                   │
│  정의:                                                           │
│  하나의 논리적 작업에서 두 개 이상의 독립된 시스템에            │
│  동시에 쓰기를 시도하는 것                                      │
│                                                                   │
│  예: DB에 저장 + 메시지 브로커에 이벤트 발행                    │
│                                                                   │
│  핵심 포인트:                                                   │
│  ├── 설정이나 코딩 실수가 아님                                  │
│  ├── 아키텍처 구조 자체에서 발생하는 근본적 문제                │
│  └── 마이크로서비스 전환 시 거의 반드시 마주치게 됨             │
│                                                                   │
│  가장 흔한 시나리오:                                            │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  주문 서비스의 일상:                                     │    │
│  │                                                          │    │
│  │  1. DB에 주문 저장          (PostgreSQL)                 │    │
│  │  2. 이벤트 발행             (Kafka)                      │    │
│  │                                                          │    │
│  │  두 작업 모두 성공해야 "주문 처리 완료"                  │    │
│  │  그런데... 이 두 시스템은 서로 다른 시스템!              │    │
│  │  하나의 트랜잭션으로 묶을 수 없다!                       │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

2. 등장 배경

2.1 모놀리스에서 마이크로서비스로의 전환

┌─────────────────────────────────────────────────────────────────┐
│        모놀리스 vs 마이크로서비스: 트랜잭션의 변화              │
│                                                                   │
│  [모놀리스 시대]                                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  ┌─────────────────────────────┐                        │    │
│  │  │      모놀리스 애플리케이션    │                        │    │
│  │  │  ┌────────┐ ┌────────┐      │                        │    │
│  │  │  │ 주문   │ │ 결제   │      │                        │    │
│  │  │  │ 모듈   │ │ 모듈   │      │                        │    │
│  │  │  └───┬────┘ └───┬────┘      │                        │    │
│  │  │      │           │           │                        │    │
│  │  │      └─────┬─────┘           │                        │    │
│  │  │            │                 │                        │    │
│  │  │      ┌─────▼─────┐          │                        │    │
│  │  │      │  단일 DB   │          │                        │    │
│  │  │      │ (ACID OK!) │          │                        │    │
│  │  │      └───────────┘          │                        │    │
│  │  └─────────────────────────────┘                        │    │
│  │                                                          │    │
│  │  단일 DB → 로컬 트랜잭션 → ACID 완벽 보장               │    │
│  │  Dual Write 문제 없음!                                   │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  [마이크로서비스 시대]                                          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  ┌──────────┐      ┌──────────┐      ┌──────────┐      │    │
│  │  │ 주문     │      │ 결제     │      │ 재고     │      │    │
│  │  │ 서비스   │─────►│ 서비스   │─────►│ 서비스   │      │    │
│  │  └────┬─────┘      └────┬─────┘      └────┬─────┘      │    │
│  │       │                 │                  │             │    │
│  │  ┌────▼─────┐      ┌───▼──────┐      ┌───▼──────┐     │    │
│  │  │ 주문 DB  │      │ 결제 DB  │      │ 재고 DB  │     │    │
│  │  └──────────┘      └──────────┘      └──────────┘     │    │
│  │                                                          │    │
│  │  Database-per-Service 패턴                               │    │
│  │  ├── 각 서비스가 자기 DB만 소유                          │    │
│  │  ├── 서비스 간 통신 필요 → DB 업데이트 + 이벤트 발행    │    │
│  │  └── DB와 메시지 브로커는 서로 다른 시스템!              │    │
│  │     → 하나의 트랜잭션으로 묶을 수 없음                   │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  Martin Kleppmann (DDIA 저자):                                  │
│  "이기종 분산 트랜잭션에서 원자성(Atomicity)이                  │
│   가장 중요한 속성이다."                                        │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

2.2 왜 문제인가: 3가지 장애 시나리오

┌─────────────────────────────────────────────────────────────────┐
│              Dual Write의 3가지 장애 시나리오                   │
│                                                                   │
│  시나리오 1: 부분 실패 (Partial Failure)                        │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  주문 서비스                                             │    │
│  │     │                                                    │    │
│  │     ├──► DB INSERT (orders) ────── OK                   │    │
│  │     │                                                    │    │
│  │     └──► Kafka PRODUCE ─────────── FAIL (브로커 다운)   │    │
│  │                                                          │    │
│  │  결과: 주문은 DB에 있지만, 하류 서비스는 모름            │    │
│  │         → 결제 안 됨, 재고 안 빠짐, 알림 안 감          │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  시나리오 2: 시스템 크래시 (Process Crash)                      │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  주문 서비스                                             │    │
│  │     │                                                    │    │
│  │     ├──► DB COMMIT ────────────── OK                    │    │
│  │     │                                                    │    │
│  │     X    (앱 크래시! OOM, SIGKILL 등)                   │    │
│  │     │                                                    │    │
│  │     └──► Kafka PRODUCE ─────────── 실행되지 않음        │    │
│  │                                                          │    │
│  │  결과: DB에는 커밋 완료, 이벤트는 영원히 유실            │    │
│  │         재시작 후에도 "어디까지 했는지" 알 수 없음       │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  시나리오 3: 네트워크 파티션 (Network Partition)                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  주문 서비스                                             │    │
│  │     │                                                    │    │
│  │     ├──► DB INSERT ────────────── OK                    │    │
│  │     │                                                    │    │
│  │     └──► Kafka PRODUCE ─────────── TIMEOUT              │    │
│  │                                                          │    │
│  │  타임아웃이면... 성공? 실패?                             │    │
│  │  ├── 실제로는 Kafka에 도달했을 수도 있음                │    │
│  │  ├── 응답만 유실된 것일 수도 있음                        │    │
│  │  └── 확인할 방법 없음 → 데이터 상태 불확실              │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  근본 원인:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  전통적 ACID 트랜잭션은 단일 시스템 내에서만             │    │
│  │  all-or-nothing을 보장한다.                              │    │
│  │                                                          │    │
│  │  Dual Write는 독립된 시스템을 관통하므로                 │    │
│  │  트랜잭션 매니저가 존재하지 않는다.                      │    │
│  │                                                          │    │
│  │  → 영구적 데이터 불일치 (Permanent Inconsistency)       │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

3. 해결 패턴 전체 지도

┌─────────────────────────────────────────────────────────────────┐
│              Dual Write 해결 패턴 전체 지도                     │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  [예방적 패턴] - 구조적으로 Dual Write를 제거           │    │
│  │  ├── Transactional Outbox  (가장 범용적)                │    │
│  │  ├── Event Sourcing        (이벤트가 곧 데이터)         │    │
│  │  ├── Listen to Yourself    (이벤트 먼저 쓰기)           │    │
│  │  └── CQRS                  (읽기/쓰기 분리)             │    │
│  │                                                          │    │
│  │  [조율 패턴] - 여러 서비스 간 일관성 조율               │    │
│  │  ├── Saga (Choreography)   (이벤트 기반 탈중앙)         │    │
│  │  ├── Saga (Orchestration)  (중앙 조율자)                │    │
│  │  └── 2PC / XA              (분산 트랜잭션)              │    │
│  │                                                          │    │
│  │  [보상적 패턴] - 불일치를 감지하고 보상                 │    │
│  │  ├── Consistency Checker   (Netflix 방식)               │    │
│  │  └── Idempotency Keys     (Stripe 방식)                │    │
│  │                                                          │    │
│  │  [수용 패턴] - Dual Write를 의도적으로 허용             │    │
│  │  └── Retry + Idempotency  (캐시 시나리오 등)            │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  관계도:                                                        │
│                                                                   │
│   예방 ◄──────── 가장 근본적 해결 ────────► 조율              │
│    │                                           │                 │
│    │  Dual Write 자체를 구조적으로 제거        │  여러 서비스 간 │
│    │  단일 서비스 내부에서 적용                 │  협력 시 사용   │
│    │                                           │                 │
│   보상 ◄──────── 불일치 발생 후 대응 ────────► 수용            │
│    │                                           │                 │
│    │  사후 감지 + 보정                         │  비핵심 데이터  │
│    │  글로벌 스케일 시스템에 적합               │  에 한해 허용   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4. Transactional Outbox 패턴 (가장 대표적)

4.1 핵심 아이디어

┌─────────────────────────────────────────────────────────────────┐
│              Transactional Outbox 패턴                          │
│                                                                   │
│  핵심 아이디어:                                                 │
│  DB 트랜잭션 안에서 비즈니스 데이터와 이벤트를 함께 저장        │
│  → 원자성 보장 (같은 DB이므로 ACID 적용)                        │
│  → 별도 프로세스가 outbox 테이블에서 이벤트를 읽어 발행        │
│                                                                   │
│  왜 이게 동작하는가?                                            │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  기존 Dual Write:                                        │    │
│  │  서비스 ──► DB (시스템 A)                                │    │
│  │  서비스 ──► Kafka (시스템 B)                              │    │
│  │  → 두 시스템에 동시에 쓰기 = 원자성 불가!               │    │
│  │                                                          │    │
│  │  Outbox 패턴:                                            │    │
│  │  서비스 ──► DB (orders + outbox 함께) = 단일 트랜잭션!  │    │
│  │  별도 프로세스 ──► outbox에서 읽어서 Kafka로 발행       │    │
│  │  → 한 시스템에만 쓰기 = 원자성 보장!                    │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4.2 동작 메커니즘

┌─────────────────────────────────────────────────────────────────┐
│              Outbox 동작 흐름                                   │
│                                                                   │
│  [STEP 1: 단일 트랜잭션으로 함께 저장]                         │
│                                                                   │
│   주문 서비스                                                   │
│      │                                                           │
│      │  BEGIN TRANSACTION                                       │
│      ├──► INSERT INTO orders (id, item, qty, status)            │
│      │    VALUES ('ORD-001', 'MacBook', 1, 'CREATED')           │
│      │                                                           │
│      ├──► INSERT INTO outbox (id, event_type, payload)          │
│      │    VALUES ('EVT-001', 'OrderCreated',                    │
│      │            '{"orderId":"ORD-001", ...}')                 │
│      │                                                           │
│      │  COMMIT  (둘 다 성공 or 둘 다 실패 = ACID!)             │
│      │                                                           │
│                                                                   │
│  [STEP 2: 별도 프로세스가 outbox에서 읽어서 발행]              │
│                                                                   │
│   outbox 테이블                                                 │
│      │                                                           │
│      │  ┌──────────────────────────────────────────┐            │
│      │  │ id       │ event_type     │ published │   │            │
│      │  │ EVT-001  │ OrderCreated   │ false     │   │            │
│      │  └──────────────────────────────────────────┘            │
│      │                                                           │
│      ▼                                                           │
│   CDC 또는 Polling 프로세스                                     │
│      │                                                           │
│      ├──► outbox에서 published=false인 행 읽기                  │
│      ├──► Kafka에 이벤트 발행                                   │
│      └──► published=true로 업데이트 (또는 DELETE)               │
│                                                                   │
│      ▼                                                           │
│   Kafka Topic: order-events                                     │
│      │                                                           │
│      ├──► 결제 서비스 (구독)                                    │
│      ├──► 재고 서비스 (구독)                                    │
│      └──► 알림 서비스 (구독)                                    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4.3 두 가지 구현 방식

Polling Publisher

┌─────────────────────────────────────────────────────────────────┐
│              방식 1: Polling Publisher                           │
│                                                                   │
│  동작 원리:                                                     │
│  주기적으로 outbox 테이블을 SELECT → 발행 → DELETE/UPDATE      │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  while (true) {                                          │    │
│  │    // 1. 미발행 이벤트 조회                              │    │
│  │    events = SELECT * FROM outbox                         │    │
│  │             WHERE published = false                      │    │
│  │             ORDER BY created_at                          │    │
│  │             LIMIT 100                                    │    │
│  │                                                          │    │
│  │    // 2. Kafka로 발행                                    │    │
│  │    for (event in events) {                               │    │
│  │      kafka.produce(event.topic, event.payload)           │    │
│  │    }                                                     │    │
│  │                                                          │    │
│  │    // 3. 발행 완료 표시                                  │    │
│  │    UPDATE outbox SET published = true                    │    │
│  │    WHERE id IN (발행된 이벤트 IDs)                       │    │
│  │                                                          │    │
│  │    sleep(1000)  // 1초 대기                              │    │
│  │  }                                                       │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  장점:                                                          │
│  ├── 어떤 DB든 사용 가능 (MySQL, PostgreSQL, Oracle 등)        │
│  ├── 인프라가 단순함 (별도 도구 불필요)                         │
│  └── 이해하기 쉬움                                              │
│                                                                   │
│  단점:                                                          │
│  ├── 지연시간 (polling 주기만큼, 보통 초 단위)                 │
│  ├── DB에 부하 (주기적 SELECT 쿼리)                             │
│  ├── 순서 보장이 약함 (동시 polling 시)                         │
│  └── 스케일링 어려움 (여러 인스턴스 시 중복 발행 위험)         │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Log-Based CDC (Debezium)

┌─────────────────────────────────────────────────────────────────┐
│              방식 2: Log-Based CDC (Debezium)                   │
│                                                                   │
│  CDC = Change Data Capture (변경 데이터 캡처)                   │
│  DB의 트랜잭션 로그(WAL/binlog)를 직접 읽는 방식               │
│                                                                   │
│  동작 원리:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  서비스 ──[트랜잭션]──┬── orders 테이블 INSERT          │    │
│  │                        └── outbox 테이블 INSERT          │    │
│  │                                                          │    │
│  │  DB 트랜잭션 로그 (WAL/binlog)                          │    │
│  │     │                                                    │    │
│  │     │  INSERT orders: {id: ORD-001, ...}                │    │
│  │     │  INSERT outbox: {id: EVT-001, type: OrderCreated} │    │
│  │     │  COMMIT                                            │    │
│  │     │                                                    │    │
│  │     ▼                                                    │    │
│  │  Debezium (Kafka Connect Source Connector)              │    │
│  │     │                                                    │    │
│  │     │  로그에서 outbox INSERT 감지                      │    │
│  │     │  → Kafka Topic으로 이벤트 발행                    │    │
│  │     │                                                    │    │
│  │     ▼                                                    │    │
│  │  Kafka Topic: order-events                              │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  장점:                                                          │
│  ├── 밀리초 단위 지연 (near real-time)                          │
│  ├── DB에 추가 부하 없음 (이미 기록되는 로그를 읽을 뿐)       │
│  ├── 완벽한 순서 보장 (트랜잭션 로그 순서 그대로)              │
│  └── 누락 불가 (로그는 DB가 관리)                               │
│                                                                   │
│  단점:                                                          │
│  ├── 인프라 복잡 (Kafka Connect + Debezium 운영 필요)          │
│  ├── DB 종류에 의존 (PostgreSQL WAL, MySQL binlog 등)          │
│  ├── 로그 보존 기간 관리 필요                                   │
│  └── 학습 곡선이 있음                                           │
│                                                                   │
│  Debezium 아키텍처:                                             │
│  ┌──────────┐    ┌───────────────┐    ┌──────────────┐         │
│  │ DB (WAL) │───►│ Kafka Connect │───►│ Kafka Topic  │         │
│  │          │    │ + Debezium    │    │              │         │
│  └──────────┘    └───────────────┘    └──────────────┘         │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4.4 Insert-Delete 최적화

┌─────────────────────────────────────────────────────────────────┐
│              Insert-Delete 최적화 기법                          │
│                                                                   │
│  문제: outbox 테이블이 계속 커짐 → 디스크 bloat               │
│                                                                   │
│  해결: 트랜잭션 내에서 INSERT 후 즉시 DELETE                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  BEGIN TRANSACTION                                       │    │
│  │                                                          │    │
│  │    INSERT INTO orders (id, item) VALUES (...)            │    │
│  │    INSERT INTO outbox (id, payload) VALUES (...)         │    │
│  │    DELETE FROM outbox WHERE id = ...  -- 즉시 삭제!     │    │
│  │                                                          │    │
│  │  COMMIT                                                  │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  왜 동작하는가?                                                 │
│  ├── DELETE해도 트랜잭션 로그(WAL/binlog)에는 기록이 남음     │
│  ├── Debezium은 로그를 읽으므로 INSERT를 캡처할 수 있음       │
│  ├── outbox 테이블 자체는 항상 비어있음                        │
│  └── 디스크 bloat 완전히 제거                                  │
│                                                                   │
│  Debezium의 Outbox Event Router:                               │
│  이 패턴을 공식 지원하는 Debezium SMT(Single Message Transform)│
│  → outbox 테이블 구조만 맞추면 자동으로 라우팅                 │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4.5 주의사항

┌─────────────────────────────────────────────────────────────────┐
│              Outbox 패턴 사용 시 주의사항                       │
│                                                                   │
│  주의 1: 소비자는 반드시 멱등성(Idempotency) 보장              │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  왜? → 중복 발행이 가능하기 때문                         │    │
│  │                                                          │    │
│  │  CDC가 이벤트를 Kafka에 발행한 후                        │    │
│  │  오프셋 커밋 전에 크래시하면?                             │    │
│  │  → 재시작 시 같은 이벤트를 다시 발행 (at-least-once)    │    │
│  │                                                          │    │
│  │  소비자가 멱등하지 않으면?                                │    │
│  │  → 같은 주문이 두 번 결제될 수 있음!                    │    │
│  │                                                          │    │
│  │  해결: eventId로 중복 체크                                │    │
│  │  IF NOT EXISTS (SELECT 1 FROM processed_events            │    │
│  │                  WHERE event_id = ?)                       │    │
│  │  THEN process(event)                                      │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  주의 2: 개발자가 outbox 쓰기를 깜빡할 수 있음                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  문제: 모든 DB 쓰기에 outbox INSERT를 함께 해야 하는데  │    │
│  │        개발자가 잊어버리면 이벤트 유실                    │    │
│  │                                                          │    │
│  │  해결:                                                   │    │
│  │  ├── Spring Modulith: @ApplicationModuleListener로       │    │
│  │  │   이벤트 발행을 프레임워크가 자동 관리                │    │
│  │  ├── 코드 리뷰에서 outbox 누락 체크                      │    │
│  │  └── 아키텍처 테스트 (ArchUnit)로 강제                   │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

5. Event Sourcing

5.1 핵심 아이디어

┌─────────────────────────────────────────────────────────────────┐
│              Event Sourcing                                     │
│                                                                   │
│  핵심 아이디어:                                                 │
│  현재 상태 대신 "변경 이벤트"를 불변 로그(Immutable Log)로     │
│  저장한다. 이벤트 로그가 Source of Truth이며,                   │
│  현재 상태는 이벤트 리플레이로 재구성한다.                      │
│                                                                   │
│  역사:                                                          │
│  ├── Martin Fowler가 2005년에 Event Sourcing 패턴 공식화       │
│  ├── Pat Helland의 "Immutability Changes Everything" 이론      │
│  └── Greg Young이 CQRS + Event Sourcing 조합을 대중화         │
│                                                                   │
│  비유:                                                          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  전통적 방식 (상태 저장):                                │    │
│  │  계좌 잔고 = 150,000원                                   │    │
│  │  → "왜 150,000원인지" 알 수 없음                        │    │
│  │                                                          │    │
│  │  Event Sourcing (이벤트 저장):                           │    │
│  │  1. 계좌 개설: +0원                                      │    │
│  │  2. 입금: +200,000원                                     │    │
│  │  3. 출금: -30,000원                                      │    │
│  │  4. 이체 수수료: -500원                                  │    │
│  │  5. 입금: +50,000원                                      │    │
│  │  6. 출금: -69,500원                                      │    │
│  │  → 현재 잔고 = 리플레이 결과 = 150,000원                │    │
│  │  → "왜 150,000원인지" 완벽하게 추적 가능                │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

5.2 Dual Write 제거 원리

┌─────────────────────────────────────────────────────────────────┐
│              Event Sourcing이 Dual Write를 제거하는 원리        │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  전통적 방식 (Dual Write 발생):                         │    │
│  │  서비스 ──► DB (상태 저장)    ← 시스템 A               │    │
│  │  서비스 ──► Kafka (이벤트)    ← 시스템 B               │    │
│  │  = 두 곳에 쓰기 = Dual Write!                           │    │
│  │                                                          │    │
│  │  Event Sourcing 방식:                                    │    │
│  │  서비스 ──► Event Store (단일 테이블에 이벤트 INSERT)   │    │
│  │             │                                            │    │
│  │             └──► 별도 프로세스가 Kafka로 발행            │    │
│  │                  (published 플래그로 관리)               │    │
│  │                                                          │    │
│  │  단일 테이블에 단일 행 INSERT                            │    │
│  │  → 본질적으로 원자적 (atomic)                           │    │
│  │  → Dual Write 근본적으로 제거!                          │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  Event Store 구조:                                              │
│  ┌──────────────────────────────────────────────────────┐       │
│  │ event_id  │ aggregate_id │ type          │ published │       │
│  │ E-001     │ ORD-001      │ OrderCreated  │ true      │       │
│  │ E-002     │ ORD-001      │ OrderPaid     │ true      │       │
│  │ E-003     │ ORD-002      │ OrderCreated  │ false     │       │
│  └──────────────────────────────────────────────────────┘       │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

5.3 장점 vs 단점

┌─────────────────────────────────────────────────────────────────┐
│              Event Sourcing 장점 vs 단점                        │
│                                                                   │
│  장점:                                                          │
│  ├── 완전한 감사 추적 (Audit Trail)                             │
│  │   → 모든 변경 이력이 불변 로그로 영구 보존                  │
│  ├── 시간 여행 (Time Travel)                                    │
│  │   → 과거 임의 시점의 상태를 재구성 가능                     │
│  ├── 이벤트 리플레이 (Replay)                                   │
│  │   → 버그 수정 후 이벤트를 다시 재생하여 데이터 복구        │
│  └── 디버깅 용이                                                │
│      → "어떤 이벤트가 이 상태를 만들었는지" 추적 가능          │
│                                                                   │
│  단점:                                                          │
│  ├── CQRS 필수                                                  │
│  │   → 읽기 모델을 별도로 유지해야 함 (프로젝션)              │
│  ├── 복잡한 아키텍처                                            │
│  │   → 이벤트 스키마 진화, 스냅샷 관리, 프로젝션 재구축      │
│  ├── 인재 부족                                                  │
│  │   → Event Sourcing 경험 있는 개발자 찾기 어려움             │
│  ├── Read-your-own-writes 불가                                  │
│  │   → 쓰기 직후 읽기 모델에 반영 안 될 수 있음               │
│  └── 이벤트 수가 많아지면 리플레이 시간 증가                   │
│      → 스냅샷 전략 필수                                         │
│                                                                   │
│  Debezium 팀의 견해:                                            │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  "대부분의 경우 CDC + Outbox가 Event Sourcing보다        │    │
│  │   더 나은 대안이다. Event Sourcing의 복잡도를             │    │
│  │   감당할 수 있는 팀이 많지 않다."                         │    │
│  │                                                          │    │
│  │   - Gunnar Morling (Debezium Creator)                    │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

6. Listen to Yourself 패턴

6.1 핵심 아이디어

┌─────────────────────────────────────────────────────────────────┐
│              Listen to Yourself 패턴                            │
│                                                                   │
│  핵심 아이디어:                                                 │
│  전통적 순서를 뒤집는다.                                        │
│  "DB 먼저"가 아닌 "이벤트 스트림 먼저" 쓰기                    │
│  서비스가 자기가 발행한 이벤트를 자기가 구독하여 DB 업데이트   │
│                                                                   │
│  비유:                                                          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  전통적 방식:                                            │    │
│  │  "일기를 쓰고(DB), 친구에게 알려준다(이벤트)"           │    │
│  │                                                          │    │
│  │  Listen to Yourself:                                     │    │
│  │  "공개 게시판에 올리고(이벤트), 내 일기장에도             │    │
│  │   게시판 보고 옮겨 적는다(DB)"                           │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

6.2 동작 흐름

┌─────────────────────────────────────────────────────────────────┐
│              Listen to Yourself 동작 흐름                       │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  클라이언트 ──► 주문 서비스                              │    │
│  │                    │                                     │    │
│  │                    ├──► Kafka에 OrderCreated 이벤트 발행 │    │
│  │                    │    (DB에는 아직 안 씀!)             │    │
│  │                    │                                     │    │
│  │                    └──► 클라이언트에 즉시 응답           │    │
│  │                         (202 Accepted)                   │    │
│  │                                                          │    │
│  │  Kafka Topic: order-events                               │    │
│  │     │                                                    │    │
│  │     ├──► 주문 서비스 (자기 자신이 구독!)                │    │
│  │     │    → DB에 주문 저장                                │    │
│  │     │                                                    │    │
│  │     ├──► 결제 서비스 (다른 서비스도 구독)               │    │
│  │     │    → 결제 처리                                     │    │
│  │     │                                                    │    │
│  │     └──► 재고 서비스                                     │    │
│  │          → 재고 차감                                     │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  포인트:                                                        │
│  ├── Kafka가 유일한 Source of Truth                              │
│  ├── DB는 Kafka의 파생(Derived) 저장소                          │
│  ├── 단일 쓰기 지점 (Kafka만) → Dual Write 제거                │
│  └── 모든 소비자(자기 자신 포함)가 동일한 이벤트 스트림 사용   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

6.3 Outbox와의 차이

┌─────────────────────────────────────────────────────────────────┐
│              Outbox vs Listen to Yourself 비교                  │
│                                                                   │
│  Transactional Outbox:                                          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Command → DB 먼저 저장 → CDC/Polling → Kafka 발행     │    │
│  │  Source of Truth = DB                                    │    │
│  │  이벤트는 DB 변경의 "파생물"                            │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  Listen to Yourself:                                            │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Command → Kafka 먼저 발행 → 자기 구독 → DB 저장       │    │
│  │  Source of Truth = Kafka                                 │    │
│  │  DB는 Kafka 이벤트의 "파생물"                           │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  비판적 시각:                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  "Outbox 패턴을 과도하게 사용하지 마라"                  │    │
│  │                                                          │    │
│  │  Outbox의 한계:                                          │    │
│  │  ├── 인프라 복잡도 (CDC 파이프라인 운영)                │    │
│  │  ├── DB 스키마에 outbox 테이블 추가 필요                │    │
│  │  └── 모든 서비스에 동일 패턴 반복 적용 부담             │    │
│  │                                                          │    │
│  │  Listen to Yourself이 더 나은 경우:                      │    │
│  │  ├── 이벤트 중심 아키텍처를 이미 채택한 시스템          │    │
│  │  ├── Kafka가 이미 핵심 인프라인 조직                    │    │
│  │  └── 최종 일관성(Eventual Consistency)을 수용 가능할 때 │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

7. Saga 패턴

7.1 핵심 아이디어

┌─────────────────────────────────────────────────────────────────┐
│              Saga 패턴                                          │
│                                                                   │
│  핵심 아이디어:                                                 │
│  여러 서비스에 걸친 분산 트랜잭션을                              │
│  로컬 트랜잭션의 연쇄(chain)로 분해한다.                       │
│  실패 시 보상 트랜잭션(compensating transaction)으로            │
│  수동 롤백한다.                                                  │
│                                                                   │
│  기원: 1987년 Garcia-Molina & Salem "SAGAS" 논문               │
│                                                                   │
│  핵심 구조:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  정상 흐름:                                              │    │
│  │  T1(주문) → T2(결제) → T3(재고) → T4(배송) = 완료     │    │
│  │                                                          │    │
│  │  T3에서 실패 시:                                         │    │
│  │  T1 → T2 → T3(X) → C2(결제취소) → C1(주문취소)        │    │
│  │  ─────정방향─────   ────────역방향 보상────────          │    │
│  │                                                          │    │
│  │  각 Ti에는 대응하는 Ci(보상 트랜잭션)가 존재            │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

7.2 두 가지 방식

Choreography (탈중앙, 이벤트 기반)

┌─────────────────────────────────────────────────────────────────┐
│              Choreography (안무, 탈중앙 방식)                   │
│                                                                   │
│  각 서비스가 도메인 이벤트를 발행 → 다음 서비스가 반응         │
│  중앙 조정자 없이 서비스들이 자율적으로 협력                    │
│                                                                   │
│  정상 흐름:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  주문서비스          결제서비스          재고서비스      │    │
│  │     │                   │                   │            │    │
│  │     │ OrderCreated      │                   │            │    │
│  │     ├──────────────────►│                   │            │    │
│  │     │                   │ PaymentCompleted  │            │    │
│  │     │                   ├──────────────────►│            │    │
│  │     │                   │                   │ StockReserved│   │
│  │     │                   │                   ├───► 완료   │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  실패 시 보상 흐름:                                             │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  주문서비스          결제서비스          재고서비스      │    │
│  │     │                   │                   │            │    │
│  │     │ OrderCreated      │                   │            │    │
│  │     ├──────────────────►│                   │            │    │
│  │     │                   │ PaymentCompleted  │            │    │
│  │     │                   ├──────────────────►│            │    │
│  │     │                   │                   │ StockFailed│    │
│  │     │                   │◄──────────────────┤            │    │
│  │     │ PaymentRefunded   │                   │            │    │
│  │     │◄──────────────────┤                   │            │    │
│  │     │ OrderCancelled    │                   │            │    │
│  │     ├───► 취소 완료     │                   │            │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  장점:                                                          │
│  ├── 단순한 구현 (이벤트 발행/구독만)                          │
│  ├── 단일 장애점(SPOF) 없음                                    │
│  └── 서비스 간 느슨한 결합                                      │
│                                                                   │
│  단점:                                                          │
│  ├── 참여자가 많아지면 흐름 추적 어려움                        │
│  ├── 전역 상태를 한눈에 파악할 수 없음                         │
│  └── 순환 의존성(Cyclic Dependency) 위험                       │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Orchestration (중앙 조율)

┌─────────────────────────────────────────────────────────────────┐
│              Orchestration (오케스트레이션, 중앙 조율 방식)     │
│                                                                   │
│  중앙 오케스트레이터가 각 서비스에 명시적으로 지시             │
│                                                                   │
│  정상 흐름:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │              주문 Saga 오케스트레이터                     │    │
│  │                       │                                  │    │
│  │          ┌────────────┼────────────┐                    │    │
│  │          │            │            │                    │    │
│  │          ▼            ▼            ▼                    │    │
│  │    "결제 처리해"  "재고 차감해"  "배송 시작해"          │    │
│  │          │            │            │                    │    │
│  │          ▼            ▼            ▼                    │    │
│  │     결제서비스    재고서비스    배송서비스               │    │
│  │          │            │            │                    │    │
│  │          └────────────┼────────────┘                    │    │
│  │                       ▼                                  │    │
│  │              오케스트레이터에 결과 보고                   │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  장점:                                                          │
│  ├── 복잡한 워크플로우 관리 용이                                │
│  ├── 타임아웃, 재시도, 보상을 중앙에서 제어                    │
│  ├── 전역 상태를 한눈에 파악 가능                               │
│  └── 디버깅과 모니터링이 쉬움                                   │
│                                                                   │
│  단점:                                                          │
│  ├── 오케스트레이터가 단일 장애점(SPOF)이 될 수 있음           │
│  ├── Temporal, Cadence 같은 전용 프레임워크 필요                │
│  └── 오케스트레이터 로직이 복잡해질 수 있음                     │
│                                                                   │
│  실무 프레임워크:                                               │
│  ├── Temporal (Uber 출신, 가장 인기)                            │
│  ├── Cadence (Uber 원본)                                        │
│  ├── Netflix Conductor                                          │
│  └── Apache Airflow (데이터 파이프라인 중심)                    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

7.3 중요: Saga의 각 단계 자체가 Dual Write

┌─────────────────────────────────────────────────────────────────┐
│              Saga 안에 숨은 Dual Write                          │
│                                                                   │
│  흔히 놓치는 사실:                                              │
│  Saga의 각 단계(Ti)에서 서비스가 수행하는 작업 자체가          │
│  Dual Write이다!                                                │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  Saga 단계 T2 (결제 서비스):                             │    │
│  │     │                                                    │    │
│  │     ├──► 결제 DB에 결제 정보 저장   (시스템 A)          │    │
│  │     │                                                    │    │
│  │     └──► Kafka에 PaymentCompleted 발행 (시스템 B)       │    │
│  │                                                          │    │
│  │  → 이것도 Dual Write!                                   │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  해결: Saga + Outbox 조합 (패턴의 계층적 합성)                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  Saga 단계 T2 (결제 서비스) + Outbox 적용:              │    │
│  │     │                                                    │    │
│  │     │  BEGIN TRANSACTION                                 │    │
│  │     ├──► 결제 DB에 결제 정보 저장                       │    │
│  │     ├──► outbox 테이블에 PaymentCompleted INSERT        │    │
│  │     │  COMMIT (단일 트랜잭션!)                           │    │
│  │     │                                                    │    │
│  │     └──► CDC가 outbox에서 읽어 Kafka 발행               │    │
│  │                                                          │    │
│  │  → Dual Write 해결!                                     │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  핵심 교훈:                                                     │
│  패턴은 계층적으로 합성(Compose)된다.                           │
│  Saga가 서비스 간 조율을 담당하고,                              │
│  Outbox가 각 서비스 내부의 원자성을 담당한다.                   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

8. 기타 해결 패턴

8.1 2PC / XA 트랜잭션

┌─────────────────────────────────────────────────────────────────┐
│              2PC / XA 트랜잭션                                  │
│                                                                   │
│  2PC = Two-Phase Commit (2단계 커밋)                            │
│  XA = eXtended Architecture (X/Open 표준)                       │
│                                                                   │
│  분산 트랜잭션 매니저가 모든 참여자의 prepare/commit을 조율    │
│                                                                   │
│  동작 원리:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  Phase 1: Prepare (준비)                                 │    │
│  │  트랜잭션 매니저 ──► "커밋할 준비 됐나?"                │    │
│  │       ├──► DB:    "준비 완료 (YES)"                     │    │
│  │       └──► Kafka: "준비 완료 (YES)"                     │    │
│  │                                                          │    │
│  │  Phase 2: Commit (확정)                                  │    │
│  │  트랜잭션 매니저 ──► "전원 준비 완료, 커밋!"            │    │
│  │       ├──► DB:    COMMIT                                │    │
│  │       └──► Kafka: COMMIT                                │    │
│  │                                                          │    │
│  │  하나라도 NO → 전원 ROLLBACK                            │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  장점:                                                          │
│  └── 강한 일관성 (Strong Consistency) 보장                     │
│                                                                   │
│  단점:                                                          │
│  ├── 성능 저하 (Prepare 동안 리소스 Lock)                      │
│  ├── 확장성 제한 (참여자 증가 시 성능 급감)                    │
│  ├── 클라우드/컨테이너 환경에 부적합                           │
│  │   (Kafka는 XA를 지원하지 않음)                              │
│  └── 트랜잭션 매니저가 SPOF                                    │
│                                                                   │
│  현대 마이크로서비스에서는 거의 사용하지 않음                  │
│  → Saga 또는 Outbox로 대체                                     │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

8.2 CQRS (Command Query Responsibility Segregation)

┌─────────────────────────────────────────────────────────────────┐
│              CQRS (명령 쿼리 책임 분리)                         │
│                                                                   │
│  핵심: 쓰기(Command)와 읽기(Query)를 완전히 분리               │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  Command (쓰기)              Query (읽기)               │    │
│  │  ┌──────────────┐           ┌──────────────┐           │    │
│  │  │ 주문 서비스   │           │ 읽기 서비스   │           │    │
│  │  │ (Command)    │           │ (Query)      │           │    │
│  │  └──────┬───────┘           └──────┬───────┘           │    │
│  │         │                          │                    │    │
│  │    ┌────▼─────┐              ┌─────▼──────┐            │    │
│  │    │ Write DB │──[Kafka]──►│  Read DB   │            │    │
│  │    │(정규화)  │  (이벤트)   │(비정규화)  │            │    │
│  │    └──────────┘              └────────────┘            │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  Dual Write 제거 원리:                                          │
│  ├── 쓰기 서비스는 Write DB에만 씀 (단일 쓰기)                │
│  ├── Kafka Streams/CDC로 읽기 저장소에 자동 복사              │
│  ├── 클라이언트가 두 곳에 쓸 필요 없음                        │
│  └── 읽기 저장소를 쿼리 패턴별로 최적화 가능                   │
│      (Neo4j, Elasticsearch, Redis 등)                           │
│                                                                   │
│  주의:                                                          │
│  ├── 읽기 모델은 쓰기 후 즉시 반영 안 될 수 있음              │
│  │   (Eventual Consistency)                                     │
│  └── 시스템 복잡도 증가                                         │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

8.3 Consistency Checker (Netflix 방식)

┌─────────────────────────────────────────────────────────────────┐
│              Consistency Checker (Netflix 방식)                 │
│                                                                   │
│  핵심: 불일치를 예방하지 않고, 지속적으로 감지하고 보상        │
│                                                                   │
│  Netflix의 접근:                                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  "정확성(Correctness)보다                                │    │
│  │   합의(Consensus)에 집중하라"                            │    │
│  │                                                          │    │
│  │  전역 규모 시스템에서는                                  │    │
│  │  예방 비용 > 탐지 + 보상 비용                            │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  동작 원리:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  시스템 A (Cassandra DC-1)                               │    │
│  │     │                                                    │    │
│  │     │◄─── Consistency Checker (백그라운드 프로세스) ───►│    │
│  │     │                                                    │    │
│  │  시스템 B (Cassandra DC-2)                               │    │
│  │                                                          │    │
│  │  주기적으로:                                             │    │
│  │  1. 양쪽 시스템의 데이터 비교                            │    │
│  │  2. 불일치 발견 시 자동 보상 (Reconciliation)           │    │
│  │  3. 메트릭 수집 및 알림                                  │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  적합한 상황:                                                   │
│  ├── 글로벌 멀티 데이터센터 운영                               │
│  ├── 완벽한 예방이 비현실적인 규모                              │
│  └── 일시적 불일치가 비즈니스에 치명적이지 않은 경우           │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

8.4 Idempotency Keys (Stripe 방식)

┌─────────────────────────────────────────────────────────────────┐
│              Idempotency Keys (Stripe 방식)                     │
│                                                                   │
│  핵심: 클라이언트가 고유 키(UUID v4)를 제공하여                │
│        중복 요청을 완벽하게 방지                                │
│                                                                   │
│  동작 원리:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  클라이언트:                                             │    │
│  │  POST /v1/charges                                        │    │
│  │  Idempotency-Key: "a1b2c3d4-e5f6-..."                   │    │
│  │  { amount: 50000, currency: "krw" }                      │    │
│  │                                                          │    │
│  │  Stripe 서버:                                            │    │
│  │  1. idempotency_keys 테이블에서 키 조회                  │    │
│  │  2. 키 없음 → 결제 처리 → 키+결과 저장                  │    │
│  │  3. 키 있음 → 저장된 결과 그대로 반환 (처리 안 함)      │    │
│  │                                                          │    │
│  │  네트워크 장애로 응답 유실 시:                           │    │
│  │  클라이언트가 동일 키로 재시도 → 이미 처리됨 → 안전!   │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  Atomic Phases 패턴 (Stripe 내부):                             │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  외부 API 호출 전후로 로컬 트랜잭션 체크포인트 생성     │    │
│  │                                                          │    │
│  │  Phase 1: 로컬 DB에 "시작" 기록                          │    │
│  │  Phase 2: 외부 PG사 API 호출 (결제 실행)                │    │
│  │  Phase 3: 로컬 DB에 "완료" 기록                          │    │
│  │                                                          │    │
│  │  Phase 2에서 타임아웃 시:                                │    │
│  │  → Phase 1 기록이 있으므로 "어디까지 했는지" 알 수 있음│    │
│  │  → 안전하게 재시도 또는 롤백 가능                       │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  결제 시스템에서 이중 결제 방지의 핵심 패턴                    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

9. 패턴 비교표

┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Dual Write 해결 패턴 비교표                                             │
│                                                                                                              │
│  패턴                 │ 원자성  │ 일관성     │ 복잡도 │ 지연    │ 순서보장 │ 인프라      │ 적합한 상황       │
│  ─────────────────────┼─────────┼────────────┼────────┼─────────┼──────────┼─────────────┼───────────────────│
│  Transactional Outbox │ 보장    │ Eventual   │ 중      │ ms~s    │ 강함     │ CDC/Polling │ 단일 서비스       │
│  (Polling)            │         │            │        │ (초)    │ (약함)   │ (간단)      │ DB+브로커         │
│  ─────────────────────┼─────────┼────────────┼────────┼─────────┼──────────┼─────────────┼───────────────────│
│  Transactional Outbox │ 보장    │ Eventual   │ 중상    │ ms      │ 강함     │ Debezium    │ 단일 서비스       │
│  (CDC/Debezium)       │         │            │        │ (밀리초)│ (완벽)   │ Kafka Connect│ 실시간 필요      │
│  ─────────────────────┼─────────┼────────────┼────────┼─────────┼──────────┼─────────────┼───────────────────│
│  Event Sourcing       │ 보장    │ Eventual   │ 상      │ ms      │ 완벽     │ Event Store │ 감사 추적 필수    │
│                       │         │            │        │         │          │ CQRS        │ 금융/보험         │
│  ─────────────────────┼─────────┼────────────┼────────┼─────────┼──────────┼─────────────┼───────────────────│
│  Listen to Yourself   │ 보장    │ Eventual   │ 중      │ ms      │ 강함     │ Kafka       │ 이벤트 중심       │
│                       │ (Kafka) │            │        │         │          │             │ 아키텍처          │
│  ─────────────────────┼─────────┼────────────┼────────┼─────────┼──────────┼─────────────┼───────────────────│
│  Saga (Choreography)  │ 없음    │ Eventual   │ 중      │ 가변    │ 약함     │ 메시지브로커│ 단순 워크플로우    │
│                       │ (보상)  │            │        │         │          │             │ 서비스 3개 이하    │
│  ─────────────────────┼─────────┼────────────┼────────┼─────────┼──────────┼─────────────┼───────────────────│
│  Saga (Orchestration) │ 없음    │ Eventual   │ 상      │ 가변    │ 강함     │ Temporal    │ 복잡 워크플로우    │
│                       │ (보상)  │            │        │         │          │ Cadence     │ 서비스 4개 이상    │
│  ─────────────────────┼─────────┼────────────┼────────┼─────────┼──────────┼─────────────┼───────────────────│
│  2PC / XA             │ 보장    │ Strong     │ 상      │ 높음    │ 완벽     │ XA 매니저   │ 레거시 시스템     │
│                       │         │            │        │ (Lock)  │          │             │ 강한 일관성 필수   │
│  ─────────────────────┼─────────┼────────────┼────────┼─────────┼──────────┼─────────────┼───────────────────│
│  CQRS                 │ N/A     │ Eventual   │ 중상    │ ms~s    │ 강함     │ Kafka       │ 읽기/쓰기 패턴    │
│                       │ (분리)  │            │        │         │          │ Streams     │ 크게 다를 때      │
│  ─────────────────────┼─────────┼────────────┼────────┼─────────┼──────────┼─────────────┼───────────────────│
│  Consistency Checker  │ 없음    │ Eventually │ 중      │ 분~시   │ 없음     │ 커스텀      │ 글로벌 스케일     │
│                       │ (사후)  │ Convergent │        │         │          │ 백그라운드  │ 예방 비용 과다    │
│  ─────────────────────┼─────────┼────────────┼────────┼─────────┼──────────┼─────────────┼───────────────────│
│  Idempotency Keys     │ 없음    │ 요청 단위  │ 하      │ 없음    │ N/A      │ UUID +      │ 결제/금융         │
│                       │ (보완)  │ 보장       │        │         │          │ 중복 체크   │ 외부 API 호출     │
│                                                                                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

10. 언제 어떤 패턴을 선택하나? (의사결정 가이드)

┌─────────────────────────────────────────────────────────────────┐
│              Dual Write 해결 패턴 의사결정 트리                 │
│                                                                   │
│  시작: "어떤 종류의 Dual Write 문제인가?"                      │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  Q1. 단일 서비스의 DB + 메시지 브로커 문제인가?         │    │
│  │      │                                                   │    │
│  │      ├── Yes ──► Transactional Outbox (가장 범용적)     │    │
│  │      │           ├── Polling: 간단한 인프라 선호 시     │    │
│  │      │           └── CDC(Debezium): 실시간 필요 시      │    │
│  │      │                                                   │    │
│  │      └── No ──► Q2로                                    │    │
│  │                                                          │    │
│  │  Q2. 여러 서비스 간 조율이 필요한가?                    │    │
│  │      │                                                   │    │
│  │      ├── Yes ──► Saga                                   │    │
│  │      │           ├── 단순 (3개 이하): Choreography      │    │
│  │      │           └── 복잡 (4개 이상): Orchestration     │    │
│  │      │           (+ 각 단계에 Outbox 조합 필수!)        │    │
│  │      │                                                   │    │
│  │      └── No ──► Q3로                                    │    │
│  │                                                          │    │
│  │  Q3. 완전한 감사 추적(Audit Trail)이 필수인가?          │    │
│  │      │                                                   │    │
│  │      ├── Yes ──► Event Sourcing                         │    │
│  │      │           (금융, 보험, 의료 등 규제 산업)        │    │
│  │      │                                                   │    │
│  │      └── No ──► Q4로                                    │    │
│  │                                                          │    │
│  │  Q4. 데이터 엔지니어링 파이프라인인가?                  │    │
│  │      │                                                   │    │
│  │      ├── Yes ──► CDC (Debezium)                         │    │
│  │      │           (DB 변경을 데이터 레이크/웨어하우스로) │    │
│  │      │                                                   │    │
│  │      └── No ──► Q5로                                    │    │
│  │                                                          │    │
│  │  Q5. 캐시 동기화 문제인가?                              │    │
│  │      │                                                   │    │
│  │      ├── Yes ──► Retry + TTL                            │    │
│  │      │           (Dual Write 허용, 비핵심 데이터)       │    │
│  │      │                                                   │    │
│  │      └── No ──► Q6로                                    │    │
│  │                                                          │    │
│  │  Q6. 결제/금융 시스템인가?                              │    │
│  │      │                                                   │    │
│  │      ├── Yes ──► Idempotency Keys + Outbox 조합         │    │
│  │      │           (이중 결제 방지 + 이벤트 발행 보장)    │    │
│  │      │                                                   │    │
│  │      └── No ──► Q7로                                    │    │
│  │                                                          │    │
│  │  Q7. 글로벌 스케일 시스템인가?                          │    │
│  │      │                                                   │    │
│  │      ├── Yes ──► Consistency Checker                    │    │
│  │      │           (Netflix 방식, 사후 보상)              │    │
│  │      │                                                   │    │
│  │      └── No ──► Transactional Outbox로 시작하라        │    │
│  │                  (가장 안전한 기본 선택)                  │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

11. Dual Write가 허용되는 경우

┌─────────────────────────────────────────────────────────────────┐
│              Dual Write를 허용해도 되는 경우                    │
│                                                                   │
│  모든 Dual Write가 문제인 것은 아니다.                          │
│  다음 조건을 만족하면 의도적으로 허용할 수 있다.                │
│                                                                   │
│  조건 1: 캐시 시나리오 (Redis 등)                              │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  서비스 ──► DB 저장 (Source of Truth)                    │    │
│  │  서비스 ──► Redis 캐시 업데이트 (파생 데이터)           │    │
│  │                                                          │    │
│  │  캐시 업데이트 실패해도:                                 │    │
│  │  ├── TTL(Time-To-Live)로 자동 만료                      │    │
│  │  ├── 다음 읽기 시 DB에서 다시 캐시 로드 (Cache-Aside)  │    │
│  │  └── 일시적 불일치만 발생 (자동 해소)                   │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  조건 2: 비핵심 데이터                                         │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  즉각적 일관성이 불필요한 데이터:                        │    │
│  │  ├── 조회수 카운터                                       │    │
│  │  ├── 최근 활동 로그                                      │    │
│  │  ├── 추천 점수 갱신                                      │    │
│  │  └── 분석용 메트릭                                       │    │
│  │                                                          │    │
│  │  이런 데이터는 잠깐 불일치해도 비즈니스 영향 없음       │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  단, 반드시 지켜야 할 것:                                      │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  1. Retry (재시도)                                       │    │
│  │     → 일시적 장애에 대응, 지수 백오프(Exponential       │    │
│  │       Backoff) + 지터(Jitter) 적용                       │    │
│  │                                                          │    │
│  │  2. 멱등성 (Idempotency) 보장                           │    │
│  │     → 재시도 시 중복 처리 방지                           │    │
│  │                                                          │    │
│  │  3. 모니터링                                             │    │
│  │     → 불일치 지속 시간과 빈도 추적                       │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

12. 실제 기업 사례

┌─────────────────────────────────────────────────────────────────┐
│              실제 기업의 Dual Write 해결 사례                   │
│                                                                   │
│  기업       │ 패턴                       │ 규모 / 용도           │
│  ───────────┼────────────────────────────┼───────────────────────│
│  Netflix    │ Consistency Checker        │ 글로벌 멀티 DC        │
│             │ + Event Sourcing           │ (Cassandra 기반)      │
│             │                            │ 불일치 사후 보상      │
│  ───────────┼────────────────────────────┼───────────────────────│
│  LinkedIn   │ Databus (CDC 선구자)       │ 2005년 개발           │
│             │                            │ 검색 인덱스 동기화    │
│             │                            │ CDC 개념의 원조       │
│  ───────────┼────────────────────────────┼───────────────────────│
│  Uber       │ Outbox +                   │ 월 120억 워크플로우   │
│             │ Cadence/Temporal           │ 실행 (Saga            │
│             │ (Saga Orchestration)       │ Orchestration)        │
│  ───────────┼────────────────────────────┼───────────────────────│
│  Stripe     │ Idempotency Keys           │ 결제 이중 방지        │
│             │ + Atomic Phases            │ 모든 API에 적용       │
│             │                            │ UUID v4 기반          │
│  ───────────┼────────────────────────────┼───────────────────────│
│  Shopify    │ Hybrid                     │ 비핵심: Polling       │
│             │ (Polling + CDC)            │ 핵심: CDC             │
│             │                            │ 상황별 전략 선택      │
│  ───────────┼────────────────────────────┼───────────────────────│
│  Zalando    │ Debezium CDC               │ 일 1000만 이벤트      │
│             │                            │ 유럽 최대 패션        │
│             │                            │ 이커머스 플랫폼       │
│                                                                   │
│  공통점:                                                        │
│  ├── 어떤 기업도 2PC/XA를 사용하지 않음                        │
│  ├── 대부분 Eventual Consistency를 수용                         │
│  ├── 규모에 따라 패턴을 조합하여 사용                           │
│  └── 멱등성은 모든 기업에서 필수로 적용                         │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

13. 정리

┌─────────────────────────────────────────────────────────────────┐
│              Dual Write 패턴 핵심 정리                          │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  1. Dual Write는 마이크로서비스 아키텍처에서             │    │
│  │     가장 흔한 데이터 불일치 원인이다.                    │    │
│  │                                                          │    │
│  │  2. 핵심 원칙:                                           │    │
│  │     "두 시스템에 동시에 쓰지 마라.                       │    │
│  │      하나에만 쓰고, 나머지는 파생시켜라."                │    │
│  │      (Write to one, derive the rest)                     │    │
│  │                                                          │    │
│  │  3. 가장 범용적 해결책:                                  │    │
│  │     Transactional Outbox + Debezium CDC                  │    │
│  │     → 대부분의 단일 서비스 Dual Write 해결 가능         │    │
│  │                                                          │    │
│  │  4. 패턴은 계층적으로 합성된다:                          │    │
│  │     Saga(서비스 간 조율) + Outbox(각 서비스 내 원자성)  │    │
│  │     → 서로 다른 레벨의 문제를 각각 해결                  │    │
│  │                                                          │    │
│  │  5. 멱등성(Idempotency)은 선택이 아닌 필수:             │    │
│  │     어떤 패턴을 쓰든 중복 처리는 반드시 발생할 수 있음  │    │
│  │     → 소비자 측 멱등성 보장 필수                         │    │
│  │                                                          │    │
│  │  6. 모든 곳에 같은 패턴을 쓸 필요 없다:                 │    │
│  │     ├── 핵심 데이터: Outbox + CDC (강한 보장)           │    │
│  │     ├── 비핵심 데이터: Retry + TTL (허용)               │    │
│  │     └── 결제/금융: Idempotency Keys + Outbox            │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  기억할 한 문장:                                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  "Dual Write를 해결하는 가장 좋은 방법은                │    │
│  │   애초에 Dual Write를 하지 않는 것이다.                 │    │
│  │   하나의 Source of Truth에만 쓰고,                       │    │
│  │   나머지는 그로부터 파생시켜라."                         │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

관련 키워드

Dual Write, 이중 쓰기, Transactional Outbox, CDC, Change Data Capture,
Debezium, Kafka Connect, Event Sourcing, Saga, Choreography, Orchestration,
CQRS, Listen to Yourself, 2PC, XA, 분산 트랜잭션, 멱등성, Idempotency,
Idempotency Key, Consistency Checker, Atomic Phases, at-least-once,
exactly-once, WAL, binlog, Polling Publisher, 보상 트랜잭션,
Compensating Transaction, Eventual Consistency, Strong Consistency,
Source of Truth, Temporal, Cadence, Spring Modulith, Kafka Streams,
마이크로서비스, Database-per-Service, 데이터 일관성, 원자성, ACID