이 글에서 얻는 것

  • “분산 트랜잭션” 문제가 왜 생기는지(서비스 분리/외부 의존성)와, 2PC 대신 Outbox/Saga가 선택되는 이유를 이해합니다.
  • Outbox 패턴을 통해 “DB 커밋은 됐는데 메시지 발행이 안 됨” 같은 실패를 구조적으로 해결할 수 있습니다.
  • Saga(Choreography/Orchestration)의 차이를 알고, 보상 트랜잭션/타임아웃/재시도/멱등을 포함한 실전 설계를 만들 수 있습니다.

0) 문제: 서비스가 나뉘면 “한 번에 커밋”이 깨진다

단일 DB 안에서는 트랜잭션으로 “원자적 커밋”이 가능합니다. 하지만 서비스가 분리되면(각자 DB), 다음이 불가능해집니다.

  • 주문 DB 업데이트
  • 결제 DB 업데이트
  • 인벤토리 DB 업데이트
  • 메시지 발행(Kafka)

를 “한 번에” 묶어 커밋하는 것.

그래서 현실적인 목표는 보통:

  • 최종적 일관성(eventual consistency) 을 만들고,
  • 실패 시에는 재시도/보상으로 정합성을 회복하며,
  • 중복/재처리에도 안전하게(멱등) 설계하는 것입니다.

1) Outbox 패턴: DB 커밋과 이벤트 기록을 “같은 트랜잭션”으로

Outbox는 간단한 아이디어입니다.

  1. 비즈니스 변경(예: 주문 생성)을 DB에 저장한다
  2. 같은 트랜잭션 안에서 outbox 테이블에 “발행할 이벤트”도 함께 저장한다
  3. 별도 퍼블리셔가 outbox를 읽어 Kafka로 발행한다

핵심 효과:

  • “DB는 커밋됐는데 이벤트 발행이 누락”되는 실패를 구조적으로 줄입니다.
  • 발행 실패는 outbox에 남아 있으니, 재시도로 회복할 수 있습니다.

1-1) outbox 테이블 예시(개념)

  • id(eventId, unique)
  • aggregate_id(예: orderId)
  • event_type
  • payload(JSON)
  • status(NEW/SENT/FAILED 등)
  • created_at, published_at
  • retry_count

1-2) 퍼블리싱 방식 2가지

A) Polling(폴링 퍼블리셔)

  • 일정 주기로 NEW 이벤트를 가져와 발행
  • 장점: 구현이 단순
  • 단점: 폴링 주기만큼 지연이 생길 수 있음

실무 팁:

  • 여러 퍼블리셔가 동시에 돌면 중복 발행이 생길 수 있으니, SELECT ... FOR UPDATE SKIP LOCKED 같은 “클레임(claim)” 패턴으로 한 번만 집도록 합니다.

B) CDC(Change Data Capture)

  • DB 트랜잭션 로그를 읽어서 outbox 변경을 스트림으로 전환(예: Debezium)
  • 장점: 지연이 낮고 폴링 오버헤드가 적음
  • 단점: 운영 복잡도 증가(커넥터/스키마/권한/장애 대응)

1-3) 중복 발행은 ‘가능’하다고 전제하자

Outbox도 완전한 exactly-once를 보장하지는 않습니다(네트워크/재시도/장애). 따라서:

  • 이벤트에는 eventId가 있어야 하고,
  • 컨슈머는 eventId 기반으로 멱등 처리해야 합니다.

2) Saga 패턴: 여러 서비스의 작업을 “단계 + 보상”으로 엮는다

Saga는 “여러 로컬 트랜잭션의 시퀀스”를 만들고, 중간에 실패하면 이전 단계를 보상 트랜잭션으로 되돌리는 패턴입니다.

예: 주문 생성 Saga

  1. 주문 생성(주문 서비스)
  2. 결제 승인(결제 서비스)
  3. 재고 차감(인벤토리 서비스)

중간 실패 시 보상:

  • 결제 실패 → 주문 취소
  • 재고 실패 → 결제 취소 + 주문 취소

2-1) Choreography(이벤트 중심)

각 서비스가 이벤트를 구독/발행하며 다음 단계를 진행합니다.

장점:

  • 중앙 조정자 없이 확장하기 쉬움

단점:

  • 흐름이 여러 서비스에 흩어져 “전체 그림”이 보이지 않기 쉬움
  • 이벤트 폭발/순서/타임아웃 관리가 어려워질 수 있음

2-2) Orchestration(중앙 조정자)

Orchestrator가 “다음 단계 호출/이벤트 발행/보상”을 명시적으로 관리합니다.

장점:

  • 흐름이 중앙에 모여 가시성이 좋고, 상태 관리가 명확

단점:

  • Orchestrator가 복잡해지고, 단일 실패 지점(SPOF)이 되지 않게 설계가 필요

워크플로 엔진(Temporal/Camunda 등)은 이 영역의 운영 부담을 줄이기 위한 선택지입니다(도입 비용은 있음).

3) 실전 설계에서 반드시 들어가야 하는 것

3-1) 보상 트랜잭션을 먼저 설계하라

“실패했을 때 어떻게 복구할지”가 정의되지 않으면 Saga는 운영에서 깨집니다.

3-2) 멱등/중복 처리

  • 재시도/리밸런스/네트워크 오류가 있으면 중복은 정상
  • 각 단계는 “같은 요청이 두 번 와도 결과가 한 번만 반영”되게 설계해야 합니다

3-3) 타임아웃/재시도/휴먼 인터벤션

  • 외부 서비스는 영원히 기다리면 안 됩니다(타임아웃)
  • 재시도는 무제한이 아니라 정책이 있어야 합니다
  • 최종적으로는 운영자가 개입할 수 있는 상태/도구가 필요합니다(대규모 시스템일수록)

3-4) 이벤트 스키마 버전 관리

이벤트는 서비스 간 계약입니다. 버전/호환 정책 없이 운영하면 업데이트 때마다 사고가 납니다.

4) 자주 하는 실수

  • Outbox 없이 “DB 커밋 후 Kafka 발행”을 동기 코드로만 처리(발행 누락/중복이 쉽게 발생)
  • Saga에서 보상 트랜잭션이 부실하거나, 멱등이 없어 중복 처리로 데이터 오염
  • 타임아웃/재시도 정책이 없어 장애가 “대기/적체”로 번짐

연습(추천)

  • 주문 생성 API에서 “주문 저장 + outbox 이벤트 저장”을 한 트랜잭션으로 묶고, 퍼블리셔로 Kafka에 발행해보기
  • eventId 기반 처리 이력 테이블을 만들어 컨슈머 멱등 처리를 구현해보기
  • 주문/결제/재고 3단계 Saga를 설계하고, 각 실패 케이스에서 어떤 보상이 필요한지 표로 정리해보기