학습 목표
분산 트랜잭션이 여러 DB·서비스에 걸친 원자성 문제를 만든다는 것을 설명할 수 있다.
2PC(Two-Phase Commit) 의 prepare·commit 단계와 블로킹·조정자 SPOF 한계를 설명할 수 있다.
3PC가 2PC를 개선하려다 복잡도만 늘리는 이유를 설명할 수 있다.
Saga(오케스트레이션·코레오그래피)와 보상 트랜잭션으로 ** eventual consistency**를 설계할 수 있다.
Transactional outbox로 DB COMMIT과 이벤트 발행을 한 로컬 TX에 묶는 이유를 설명할 수 있다.
문제 상황
- 주문 DB COMMIT 후 결제 API 타임아웃 — 주문만 생성됨
- 두 시스템 하나의 ROLLBACK 불가
- 샤드 A·B에 한 번에 INSERT — cross-shard ACID 기대
- PostgreSQL 한 인스턴스 밖은 기본 2PC 없음
- XA 2PC 도입 — coordinator 다운 시 participant prepare 상태 고착
- 결제 취소 API 없이 saga 보상 설계 — 돈만 빠짐
UPDATE orders+kafka.send— DB 커밋 후 MQ 장애 → 이벤트 유실
앞서 ACID는 한 DBMS 안이었다. 샤딩·MSA — 로컬 TX만 보장, 전역 원자성은 별도 패턴.
1. 분산 트랜잭션이란
분산 트랜잭션 — 두 개 이상의 독립 리소스(DB·큐·외부 API)에 걸친 작업을 하나의 단위처럼 다루려는 시도.

| 로컬 TX | 분산 TX | |
|---|---|---|
| 범위 | 한 DB connection | 여러 노드·서비스 |
| ACID | 엔진이 보장 | 프로토콜·패턴 필요 |
| 실패 | ROLLBACK 한 번 | 부분 커밋·보상 |
| 예 | BEGIN … COMMIT |
2PC, saga, outbox |
Goal: all succeed OR all undo
Reality: network · partial failure · no global lock
- 앞서 샤딩 — cross-shard JOIN·TX 비용 — 설계로 줄임
- CAP — 분할 시 강한 일관성 vs 가용성 — Architecture 일관성 모델과 연결
2. Two-Phase Commit (2PC)
2PC — 조정자(coordinator) 가 participant에게 두 단계로 전원 commit/abort 합의.

Phase 1 — Prepare
Coordinator → each participant: PREPARE
Participant → write undo/redo, lock resources, vote YES/NO
Phase 2 — Commit / Abort
All YES → COMMIT (each participant)
Any NO or timeout → ABORT (rollback)
| 역할 | |
|---|---|
| Coordinator | 트랜잭션 시작·결정 |
| Participant | prepare 투표 · commit/abort 실행 |
-- XA-style (conceptual — rare in app code)
-- xa start 'xid';
-- UPDATE account_a ...
-- xa end 'xid'; xa prepare 'xid';
-- (coordinator collects votes)
-- xa commit 'xid'; or xa rollback 'xid';
2PC 한계
| 문제 | |
|---|---|
| Blocking | coordinator 죽으면 participant prepare 상태 잠금 |
| SPOF | coordinator 가용성 |
| 성능 | 2 RTT + disk sync · 잠금 장기 |
| 운영 | heuristic commit·in-doubt 복구 수동 |
| MSA | 서비스 경계마다 2PC — 결합·지연 |
- PostgreSQL — prepared transaction — 드물게 쓰임, 모니터링 필요
- 실무 MSA — 2PC/XA 회피 — saga·outbox
3. Three-Phase Commit (3PC)
3PC — 2PC blocking 완화 시도 — CanCommit · PreCommit · DoCommit (교재·이론).
| 2PC | 3PC | |
|---|---|---|
| 단계 | 2 | 3 |
| blocking | coordinator fail → block | 완전 해소 아님 |
| 네트워크 분할 | in-doubt | non-blocking 주장도 한계 |
| 실무 | XA, DB 내부 | 거의 미사용 |
- 결론 — 강한 분산 원자성은 비용 큼 — 피하고 saga·idempotency
4. Saga
Saga — 로컬 TX 체인 + 실패 시 보상(compensating) TX — 전역 rollback 대신.

| Orchestration | Choreography | |
|---|---|---|
| 조율 | 중앙 saga orchestrator | 이벤트로 분산 |
| 흐름 | 명령 순서 명시 | OrderPlaced → ReserveStock … |
| 가시성 | 한 곳 상태 | 흩어진 consumer |
| 적합 | 복잡 워크플로 | 단순 이벤트 체인 |
Happy path:
T1: create order (local COMMIT)
T2: charge payment (local COMMIT)
T3: reserve stock (local COMMIT)
Failure at T3:
C2: refund payment (compensate)
C1: cancel order (compensate)
# code/saga-compensation.example (orchestrator sketch)
steps = [
("create_order", "cancel_order"),
("charge_payment", "refund_payment"),
("reserve_stock", "release_stock"),
]
for forward, compensate in steps:
try:
run(forward, ctx)
except Exception:
for done_forward, done_comp in reversed(completed):
run(done_comp, ctx)
raise
completed.append((forward, compensate))
| 설계 | |
|---|---|
| 보상 | 비즈니스 취소 — DELETE가 아님 반대 거래 |
| 멱등 | compensate 재시도 safe |
| 순서 | 역순 보상 |
| 격리 | 중간 상태 노출 — UI pending |
- Architecture 동기·비동기에서 saga 흐름 — DB 관점은 각 step = 로컬 TX
5. Transactional Outbox
Outbox — 비즈니스 row + outbox event row를 같은 DB TX에 COMMIT → 별도 publisher가 MQ 전송.

-- code/outbox-table.sql
BEGIN;
INSERT INTO orders (id, user_id, amount) VALUES (1, 42, 9900);
INSERT INTO outbox (id, aggregate_type, aggregate_id, payload, created_at)
VALUES (
gen_random_uuid(),
'Order',
'1',
'{"event":"OrderPlaced","orderId":1}',
now()
);
COMMIT;
-- poller / Debezium reads outbox → publishes to Kafka
| Dual write (bad) | Outbox | |
|---|---|---|
| DB + MQ | 두 번 쓰기 — 하나 실패 | 한 TX |
| 일관성 | 유실·중복 | at-least-once + idempotent consumer |
| CDC | — | Debezium outbox router |
Same DB transaction:
orders row + outbox row → COMMIT
Async:
publisher → message bus → saga next step
- Inbox — consumer 중복 처리 — processed_messages 테이블
- Ordering — partition key = aggregate_id
6. 선택 가이드
| 상황 | 권장 |
|---|---|
| 한 DB | 로컬 TX — 기본 |
| Cross-shard | avoid — 샤드 키 설계 · aggregate per shard |
| MSA 2~3 서비스 | Saga + outbox |
| 강한 전역 ACID | 한 DB로 모델 재설계 — 2PC 최후 |
| 읽기 | ** eventual OK** — lag 허용 (앞서 복제) |
| 안티패턴 | |
|---|---|
| 분산 2PC everywhere | blocking·ops |
| 보상 없는 saga | 돈·재고 불일치 |
| MQ 먼저 send | DB rollback 시 유령 이벤트 |
| Long saga | 중간 상태 hours — 타임아웃·만료 |
7. 체크리스트
| 항목 | 확인 |
|---|---|
| 경계 | 한 aggregate = 한 shard? |
| 실패 | 각 step compensate 정의? |
| Outbox | business + event same TX? |
| Consumer | idempotent? |
| 2PC | 정말 필요한지 — ADR |
| Monitor | prepared txn · outbox lag |
8. 정리
- 분산 TX — 로컬 ACID 합 ≠ 전역 ACID
- 2PC — prepare/commit — blocking·SPOF — MSA 드묾
- 3PC — 이론적 — 실무 거의 없음
- Saga — 로컬 TX + 보상 — orchestration vs choreography
- Outbox — COMMIT과 이벤트 한 TX — saga·CDC 연결
다음에 다룰 것
- NoSQL 개요
- KV·문서·CAP, 워크로드별 선택
해당 내용은 Database System Concepts, 7/E (Avraham Silberschatz, Henry F. Korth, S. Sudarshan), Designing Data-Intensive Applications (Martin Kleppmann) 의 내용을 기반으로 합니다.
'데이터베이스 따라잡기' 카테고리의 다른 글
| Connection Pool과 ORM (0) | 2026.07.05 |
|---|---|
| NoSQL 개요 (0) | 2026.07.04 |
| 파티셔닝과 샤딩 (0) | 2026.07.02 |
| 복제 (0) | 2026.07.01 |
| MVCC (0) | 2026.06.30 |