ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Life Beyond Distributed Transactions: 분산 트랜잭션 없이 살아가기
    이코에코(Eco²)/Foundations 2025. 12. 21. 18:58

    원문: Life Beyond Distributed Transactions: An Apostate's Opinion - Pat Helland (CIDR 2007)

     


    들어가며

    2007년, Microsoft의 아키텍트 Pat Helland는 분산 시스템 커뮤니티에 "배교자의 의견(An Apostate's Opinion)"이라는 제목의 논문을 발표했다. 그는 수십 년간 분산 트랜잭션의 주도자였지만, 이제 그것이 대규모 시스템에서는 불가능하다고 선언한 것이다.

    이 논문이 중요한 이유는 현대 마이크로서비스 아키텍처의 이론적 기반을 제공했기 때문이다. "왜 도메인 간 트랜잭션을 피해야 하는가?", "왜 Eventual Consistency를 받아들여야 하는가?"에 대한 근본적인 방향을 제시한다.


    분산 트랜잭션의 환상

    2PC의 약속

    전통적인 분산 트랜잭션(Two-Phase Commit, 2PC)은 아름다운 약속을 한다:

    ┌─────────────────────────────────────────────────────────────┐
    │                 2PC의 이상적인 동작                          │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  Coordinator                                                │
    │      │                                                      │
    │      │  Phase 1: "커밋해도 될까?"                           │
    │      ├─────────────────┬─────────────────┐                 │
    │      ▼                 ▼                 ▼                 │
    │  ┌───────┐        ┌───────┐        ┌───────┐             │
    │  │ DB A  │        │ DB B  │        │ DB C  │             │
    │  │ "Yes" │        │ "Yes" │        │ "Yes" │             │
    │  └───┬───┘        └───┬───┘        └───┬───┘             │
    │      │                │                │                  │
    │      └────────────────┴────────────────┘                  │
    │                       │                                    │
    │                       ▼                                    │
    │  Phase 2: "모두 커밋하라!"                                 │
    │                                                             │
    │  결과: 모든 DB가 동시에 커밋 또는 롤백                      │
    │        → 완벽한 일관성!                                    │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    현실의 문제

    하지만 현실에서 2PC는 심각한 문제를 가진다:

    ┌─────────────────────────────────────────────────────────────┐
    │                 2PC의 현실적인 문제                          │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  1. 가용성 문제: Coordinator가 죽으면?                      │
    │     ┌───────────────────────────────────────────┐          │
    │     │ Coordinator ──X── (장애)                  │          │
    │     │                                           │          │
    │     │  DB A: "Yes 했는데... 커밋? 롤백?"        │          │
    │     │  DB B: "잠금 걸린 채로 대기..."           │          │
    │     │  DB C: "무한 대기..."                     │          │
    │     │                                           │          │
    │     │  → 모든 참여자가 블로킹!                  │          │
    │     └───────────────────────────────────────────┘          │
    │                                                             │
    │  2. 성능 문제: 모든 참여자가 잠금을 유지                    │
    │     • N개 시스템 × 평균 지연시간 = 전체 지연               │
    │     • 가장 느린 시스템이 병목                               │
    │     • 확장성 제한                                          │
    │                                                             │
    │  3. 확장성 문제: 참여자가 늘어날수록                        │
    │     • 장애 확률 증가 (N개 중 1개만 실패해도 롤백)           │
    │     • 잠금 경합 증가                                        │
    │     • 네트워크 라운드트립 증가                              │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    Pat Helland의 핵심 통찰:

    "무한히 확장 가능한 시스템에서 분산 트랜잭션은 불가능하다.
    이것은 기술의 한계가 아니라 물리적 한계다."


    Entity와 Activity

    Entity: 트랜잭션의 경계

    Helland는 Entity라는 개념을 제시한다. Entity는 단일 트랜잭션 내에서 원자적으로 업데이트될 수 있는 데이터의 집합이다.

    ┌─────────────────────────────────────────────────────────────┐
    │                     Entity의 특성                            │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  Entity = 트랜잭션 경계                                     │
    │                                                             │
    │  ┌─────────────────────────────────────────────────────┐   │
    │  │                    Entity: 주문                      │   │
    │  │  ┌─────────────┬─────────────┬─────────────┐       │   │
    │  │  │   주문정보   │   주문상태   │  주문항목들  │       │   │
    │  │  │   order_id  │   status    │   items[]   │       │   │
    │  │  └─────────────┴─────────────┴─────────────┘       │   │
    │  │                                                     │   │
    │  │  → 이 안에서는 ACID 트랜잭션 가능!                  │   │
    │  └─────────────────────────────────────────────────────┘   │
    │                                                             │
    │  ┌─────────────────────────────────────────────────────┐   │
    │  │                   Entity: 재고                       │   │
    │  │  ┌─────────────┬─────────────┐                     │   │
    │  │  │   상품ID    │    수량     │                     │   │
    │  │  │ product_id  │  quantity   │                     │   │
    │  │  └─────────────┴─────────────┘                     │   │
    │  │                                                     │   │
    │  │  → 이것도 독립적인 ACID 트랜잭션                    │   │
    │  └─────────────────────────────────────────────────────┘   │
    │                                                             │
    │  주문 Entity ←─X─→ 재고 Entity                             │
    │  (두 Entity를 하나의 트랜잭션으로 묶을 수 없음!)            │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    Eric Evans가 DDD에서 말한 Aggregate의 개념과 대응된다:

    Pat Helland (2007) Eric Evans DDD (2003)
    Entity Aggregate
    Entity 내부 Aggregate 내부
    단일 트랜잭션 트랜잭션 일관성 경계
    Entity 간 Aggregate 간
    메시지로 통신 이벤트/메시지로 통신

    Activity: Entity 간의 협력

    Activity는 여러 Entity가 협력하여 비즈니스 목표를 달성하는 과정이다.

    ┌─────────────────────────────────────────────────────────────┐
    │                   Activity: 주문 처리                        │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  시간 →                                                     │
    │                                                             │
    │  [주문 Entity]        [재고 Entity]        [배송 Entity]    │
    │       │                    │                    │          │
    │       │  1. 주문 생성      │                    │          │
    │       │  (ACID 트랜잭션)   │                    │          │
    │       ├────────────────────▶                    │          │
    │       │  2. "재고 차감 요청" 메시지              │          │
    │       │                    │                    │          │
    │       │                    │  3. 재고 차감      │          │
    │       │                    │  (ACID 트랜잭션)   │          │
    │       │                    ├────────────────────▶          │
    │       │                    │  4. "배송 준비 요청" 메시지   │
    │       │                    │                    │          │
    │       │                    │                    │  5. 배송 생성
    │       │                    │                    │  (ACID)  │
    │       │                    │                    │          │
    │       ▼                    ▼                    ▼          │
    │                                                             │
    │  각 단계는 독립적인 트랜잭션                                │
    │  전체 Activity는 결과적 일관성(Eventual Consistency)        │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    핵심 아이디어:

    1. Entity 내부: 강한 일관성 (ACID)
    2. Entity 간: 결과적 일관성 (Eventual Consistency)
    3. 통신 수단: 메시지 (비동기)

    Uncertainty: 불확실성을 받아들이기

    분산 시스템의 근본적 불확실성

    Helland는 분산 시스템에서 불확실성은 피할 수 없다고 강조한다:

    ┌─────────────────────────────────────────────────────────────┐
    │               분산 시스템의 불확실성                          │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  Service A ─────────────────▶ Service B                     │
    │             "요청 보냈는데..."                               │
    │                                                             │
    │  경우 1: 요청이 도착하지 않음                               │
    │          A: 모름  │  B: 요청 없음                           │
    │                                                             │
    │  경우 2: 요청 도착, 처리 중 B 장애                          │
    │          A: 모름  │  B: 처리 안 됨                          │
    │                                                             │
    │  경우 3: 요청 처리 완료, 응답 유실                          │
    │          A: 모름  │  B: 처리 완료됨                         │
    │                                                             │
    │  경우 4: 요청 처리 완료, 응답 도착                          │
    │          A: 성공  │  B: 처리 완료됨                         │
    │                                                             │
    │  → A 입장에서 "응답 없음"은 1, 2, 3 중 어떤 것인지 모름!    │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    해결책: 멱등성(Idempotency)

    불확실성을 다루는 핵심 전략은 멱등성이다:

    ┌─────────────────────────────────────────────────────────────┐
    │                      멱등성의 힘                             │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  멱등성 = "같은 요청을 여러 번 보내도 결과가 같다"          │
    │                                                             │
    │  ❌ 비멱등 연산:                                            │
    │     "잔액에 100원 추가"                                     │
    │     1회: 100원  →  200원                                   │
    │     2회: 200원  →  300원  (다른 결과!)                     │
    │                                                             │
    │  ✅ 멱등 연산:                                              │
    │     "주문 #123의 상태를 '완료'로 변경"                      │
    │     1회: 진행중 →  완료                                    │
    │     2회: 완료   →  완료   (같은 결과!)                     │
    │                                                             │
    │  ✅ 멱등성 키 사용:                                         │
    │     "요청 #ABC-123: 잔액에 100원 추가"                      │
    │     1회: 처리 (100원 추가)                                 │
    │     2회: 이미 처리됨, 무시                                 │
    │                                                             │
    │  구현:                                                      │
    │     if request_id in processed_requests:                   │
    │         return cached_response  # 중복 요청                │
    │     else:                                                   │
    │         result = process(request)                          │
    │         processed_requests.add(request_id)                 │
    │         return result                                       │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    교환 법칙(Commutativity)

    순서 독립성

    또 다른 전략은 교환 법칙을 활용하는 것이다:

    ┌─────────────────────────────────────────────────────────────┐
    │                    교환 법칙 활용                            │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  문제: 메시지 순서가 뒤바뀌면?                              │
    │                                                             │
    │  원래 순서:                                                 │
    │  [1] 재고 10개 추가                                        │
    │  [2] 재고 3개 차감                                         │
    │  결과: 10 + (-3) = 7개                                     │
    │                                                             │
    │  뒤바뀐 순서:                                               │
    │  [2] 재고 3개 차감                                         │
    │  [1] 재고 10개 추가                                        │
    │  결과: (-3) + 10 = 7개  (같음!)                            │
    │                                                             │
    │  → 덧셈/뺄셈은 교환 법칙 성립                              │
    │  → 순서가 바뀌어도 최종 결과 동일                          │
    │                                                             │
    │  반례 (교환 법칙 불성립):                                   │
    │  [1] 상태를 "처리중"으로 변경                              │
    │  [2] 상태를 "완료"로 변경                                  │
    │  결과: 완료                                                 │
    │                                                             │
    │  뒤바뀐 순서:                                               │
    │  [2] 상태를 "완료"로 변경                                  │
    │  [1] 상태를 "처리중"으로 변경                              │
    │  결과: 처리중  (다름!)                                     │
    │                                                             │
    │  → 상태 변경은 순서 의존적                                 │
    │  → 타임스탬프나 버전 번호로 해결                           │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    Workflow: 장기 실행 작업

    Tentative Operations

    장기 실행 작업에서는 잠정적 연산(Tentative Operations)을 사용한다:

    ┌─────────────────────────────────────────────────────────────┐
    │              Tentative Operations 패턴                       │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  예: 항공권 예약 시스템                                     │
    │                                                             │
    │  1단계: 잠정적 예약 (Tentative)                             │
    │  ┌─────────────────────────────────────────────────────┐   │
    │  │ 좌석 상태: "잠정 예약됨"                             │   │
    │  │ 유효 기간: 15분                                      │   │
    │  │ 예약자: user_123                                     │   │
    │  └─────────────────────────────────────────────────────┘   │
    │                                                             │
    │  2단계: 결제 처리 (다른 Entity)                             │
    │  ┌─────────────────────────────────────────────────────┐   │
    │  │ 결제 상태: "처리 중" → "완료"                        │   │
    │  └─────────────────────────────────────────────────────┘   │
    │                                                             │
    │  3단계: 확정 또는 취소                                      │
    │  ┌─────────────────────────────────────────────────────┐   │
    │  │ 결제 성공 → 좌석 상태: "확정 예약됨"                 │   │
    │  │ 결제 실패 → 좌석 상태: "예약 가능"                   │   │
    │  │ 시간 초과 → 좌석 상태: "예약 가능" (자동 취소)       │   │
    │  └─────────────────────────────────────────────────────┘   │
    │                                                             │
    │  핵심: 각 단계는 독립적인 트랜잭션                          │
    │        전체 과정은 결과적 일관성                            │
    │        실패 시 보상 트랜잭션으로 복구                       │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    이것이 바로 1987년 Garcia-Molina가 제안한 Saga 패턴의 현대적 재해석이다.


    Almost-Infinite Scaling

    확장성의 열쇠

    Helland의 가장 중요한 통찰: 무한 확장을 위해서는 Entity 간 독립성이 필수

    ┌─────────────────────────────────────────────────────────────┐
    │                확장 가능한 아키텍처                          │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  ❌ 분산 트랜잭션 사용 시:                                  │
    │                                                             │
    │  규모 ↑  →  참여자 수 ↑  →  실패 확률 ↑  →  성능 ↓        │
    │                                                             │
    │  10개 서비스, 각 99% 가용성                                │
    │  전체 성공률: 0.99^10 = 90.4%                              │
    │  → 10% 실패!                                               │
    │                                                             │
    │  ✅ Entity 분리 + 메시징 사용 시:                          │
    │                                                             │
    │  각 Entity가 독립적으로 확장                                │
    │  실패해도 해당 Entity만 영향                                │
    │  재시도로 결과적 일관성 달성                                │
    │                                                             │
    │  ┌────────┐   ┌────────┐   ┌────────┐                    │
    │  │Entity A│   │Entity B│   │Entity C│                    │
    │  │ ×100   │   │ ×50    │   │ ×200   │                    │
    │  └───┬────┘   └───┬────┘   └───┬────┘                    │
    │      │            │            │                          │
    │      └────────────┴────────────┘                          │
    │                   │                                        │
    │              Message Queue                                 │
    │           (독립적 확장 가능)                               │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    핵심 개념 정리

    개념 설명
    Entity 단일 트랜잭션 경계, DDD의 Aggregate와 동일
    Activity 여러 Entity의 협력, 결과적 일관성
    Uncertainty 분산 시스템의 근본적 불확실성
    Idempotency 중복 요청에도 같은 결과 보장
    Commutativity 순서 독립적 연산 설계
    Tentative Operations 잠정적 연산 + 확정/취소

    더 읽을 자료


    부록: Eco² 적용 포인트

    전환 계획: gRPC → Command-Event Separation

    Eco²는 Command-Event Separation 아키텍처로 전환한다. (Kafka=Event, RabbitMQ=Command)

    ┌─────────────────────────────────────────────────────────────┐
    │       Eco² Command-Event Separation 아키텍처 (TO-BE)         │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐        │
    │  │  Scan API   │  │ Character   │  │   My API    │        │
    │  │  (Entity)   │  │   (Entity)  │  │ (Projection)│        │
    │  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘        │
    │         │                │                │                │
    │         │   ❌ 직접 트랜잭션/gRPC 불가!   │                │
    │         │                │                │                │
    │    ┌────┴────┐           │                │                │
    │    │         │           │                │                │
    │    ▼         ▼           ▼                ▼                │
    │ ┌──────┐ ┌──────────┐ ┌──────────────────────────────┐    │
    │ │Rabbit│ │  Event   │ │           Kafka              │    │
    │ │ MQ   │ │  Store   │ │       (CDC/Debezium)         │    │
    │ │      │ │ + Outbox │─┤                              │    │
    │ │ Task │ └──────────┘ │  eco2.events.scan            │    │
    │ │Queue │              │  eco2.events.character       │    │
    │ └──┬───┘              └──────────────┬───────────────┘    │
    │    │                                 │                     │
    │    ▼                    ┌────────────┼────────────┐        │
    │ ┌──────────┐            ▼            ▼            ▼        │
    │ │  Celery  │       Character    My Service   Analytics    │
    │ │ Workers  │       Consumer     Consumer     Consumer     │
    │ │          │                                               │
    │ │• Vision  │  AI Pipeline 완료 → Event Store 저장         │
    │ │• LLM     │  → Kafka 발행 (CDC) → Consumer 처리          │
    │ └──────────┘                                               │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    역할 분담: Event vs Command

    ┌─────────────────────────────────────────────────────────────┐
    │              Pat Helland 원칙 적용                           │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  Command (RabbitMQ)              Event (Kafka)              │
    │  ──────────────────              ─────────────              │
    │  "이 이미지를 분류해"            "스캔이 완료되었다"        │
    │                                                             │
    │  • ProcessImage Task             • ScanCompleted Event      │
    │  • SendEmail Task                • CharacterGranted Event   │
    │  • GenerateReport Task           • PointsAdded Event        │
    │                                                             │
    │  하나의 Worker가 처리            여러 Consumer가 구독       │
    │  처리 후 삭제                    영구 보존 (Replay)         │
    │  Retry/DLQ 내장                  Offset 기반 재처리         │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    Celery Task → Event Store 연동

    # domains/scan/tasks/ai_pipeline.py
    
    @celery_app.task(bind=True, max_retries=3)
    def process_image(self, task_id: str, image_url: str):
        """AI 파이프라인 (RabbitMQ + Celery)"""
    
        try:
            # 1. Vision API 호출 (2-5초)
            classification = vision_api.analyze(image_url)
    
            # 2. LLM 답변 생성 (3-10초)
            answer = llm_api.generate(classification)
    
            # 3. 완료 시 Event Store + Outbox 저장
            async with db.begin():
                task = await event_store.load(ScanTask, task_id)
                task.complete(classification, answer)
    
                events = task.collect_events()
                await event_store.save(task, events)
                await outbox.append_all(events)
    
            # 트랜잭션 커밋 → Debezium CDC가 Kafka로 발행
            # → Character Consumer가 보상 지급
    
        except Exception as exc:
            raise self.retry(exc=exc)

    Kafka Consumer (Idempotent)

    # domains/character/consumers/scan_consumer.py
    
    class ScanEventConsumer:
        """Kafka Consumer - ScanCompleted 이벤트 처리"""
    
        async def handle(self, event: ScanCompleted):
            if await self.is_processed(event.event_id):
                return
    
            async with self.db.begin():
                user_char = await self.event_store.load(
                    UserCharacter, event.user_id
                )
                user_char.grant_reward(event.classification["category"])
    
                await self.event_store.save(user_char, user_char.collect_events())
                await self.mark_processed(event.event_id)

    Pat Helland 원칙의 실현

    원칙 AS-IS (gRPC) TO-BE (Command-Event Separation)
    Entity 분리 gRPC 동기 호출 Kafka Event로 완전 분리
    메시지 기반 Circuit Breaker Kafka(Event) + RabbitMQ(Command)
    멱등성 Redis TTL Event ID + processed_events
    결과적 일관성 부분적 완전한 Eventual Consistency
    Tentative Ops Redis 상태 Celery Task State + Event Store
    Uncertainty 대응 Retry + DLQ Celery DLQ + Kafka 재처리
    긴 작업 gRPC Timeout RabbitMQ + Celery Worker

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango