이 글에서 얻는 것
- “분산 트랜잭션” 문제가 왜 생기는지(서비스 분리/외부 의존성)와, 2PC 대신 Outbox/Saga가 선택되는 이유를 이해합니다.
- Outbox 패턴을 통해 “DB 커밋은 됐는데 메시지 발행이 안 됨” 같은 실패를 구조적으로 해결할 수 있습니다.
- Saga(Choreography/Orchestration)의 차이를 알고, 보상 트랜잭션/타임아웃/재시도/멱등을 포함한 실전 설계를 만들 수 있습니다.
0) 문제: 서비스가 나뉘면 “한 번에 커밋”이 깨진다
단일 DB 안에서는 트랜잭션으로 “원자적 커밋”이 가능합니다. 하지만 서비스가 분리되면(각자 DB), 다음이 불가능해집니다.
- 주문 DB 업데이트
- 결제 DB 업데이트
- 인벤토리 DB 업데이트
- 메시지 발행(Kafka)
를 “한 번에” 묶어 커밋하는 것.
그래서 현실적인 목표는 보통:
- 최종적 일관성(eventual consistency) 을 만들고,
- 실패 시에는 재시도/보상으로 정합성을 회복하며,
- 중복/재처리에도 안전하게(멱등) 설계하는 것입니다.
1) Outbox 패턴: DB 커밋과 이벤트 기록을 “같은 트랜잭션”으로
Outbox는 간단한 아이디어입니다.
- 비즈니스 변경(예: 주문 생성)을 DB에 저장한다
- 같은 트랜잭션 안에서 outbox 테이블에 “발행할 이벤트”도 함께 저장한다
- 별도 퍼블리셔가 outbox를 읽어 Kafka로 발행한다
핵심 효과:
- “DB는 커밋됐는데 이벤트 발행이 누락”되는 실패를 구조적으로 줄입니다.
- 발행 실패는 outbox에 남아 있으니, 재시도로 회복할 수 있습니다.
1-1) outbox 테이블 예시(개념)
id(eventId, unique)aggregate_id(예: orderId)event_typepayload(JSON)status(NEW/SENT/FAILED 등)created_at,published_atretry_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
- 주문 생성(주문 서비스)
- 결제 승인(결제 서비스)
- 재고 차감(인벤토리 서비스)
중간 실패 시 보상:
- 결제 실패 → 주문 취소
- 재고 실패 → 결제 취소 + 주문 취소
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를 설계하고, 각 실패 케이스에서 어떤 보상이 필요한지 표로 정리해보기
💬 댓글