여러 노드·서비스에 걸친 변경을 어떻게 일관되게 만들 것인가에 대한 두 답.
2PC — 조정자가 전부의 동의를 받는다
[1단계 준비(prepare)]
- 조정자 → 각 참여자: "커밋할 수 있나?"
- 참여자 → 준비 완료하고 락을 쥔 채 "예" 라고 답한다
[2단계 커밋]
- 전원이 "예" → 조정자가 커밋 명령
- 하나라도 "아니오" → 전원 롤백
원자성이 지켜진다. 그런데 결정적 약점이 있다.
조정자가 1단계와 2단계 사이에 죽으면?
- 참여자들은 락을 쥔 채 무한정 기다린다 (블로킹)
- 그동안 그 행들에 접근하는 모든 트랜잭션이 멈춘다
그래서 MSA에서는 2PC를 피하는 것이 기본이다. 한 서비스의 장애가 다른 서비스를 얼려 버리기 때문이다.
-- PostgreSQL 도 2PC 를 지원하기는 한다
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 'A';
PREPARE TRANSACTION 'tx1'; -- 1단계. 이 상태로 남으면 락이 유지된다
COMMIT PREPARED 'tx1'; -- 2단계
-- 미결 트랜잭션 감시 (방치하면 VACUUM 까지 막는다)
SELECT * FROM pg_prepared_xacts;
max_prepared_transactions가 기본 0이라 PostgreSQL은 2PC가 기본 비활성이다 — 그만큼 권장되지 않는다는 신호다.
Saga — 되돌리기로 대신한다
주문 생성 → 결제 승인 → 재고 차감 → 배송 요청
↓ 실패
결제 취소 ← 주문 취소 (보상 트랜잭션을 역순으로)
각 단계가 독립된 로컬 트랜잭션이고, 실패하면 앞 단계를 의미적으로 되돌리는 보상 작업을 실행한다.
- 2PC — 원자성 O · 락 유지 · 블로킹 위험
- Saga — 원자성 X · 락 없음 · 중간 상태가 외부에 보인다 (최종 일관성)
Saga의 대가는 중간 상태가 노출된다는 것이다. "결제는 됐는데 재고가 없어 곧 취소됨" 같은 상태를 사용자가 볼 수 있고, 그걸 UX로 설계해야 한다.
보상은 취소가 아니다
계좌에서 100원을 뺐다 ❌ 롤백 — 그런 건 없다. 이미 커밋됐다 ✅ 보상 — 100원을 다시 넣는 새 트랜잭션을 실행한다
메일을 보냈다
- 보상이 불가능하다. 이런 작업은 Saga 의 맨 뒤에 둔다
되돌릴 수 없는 작업은 순서상 마지막에 배치하는 것이 Saga 설계의 기본 원칙이다.
면접 함정
- ❌ "Saga는 ACID를 보장한다" → 원자성을 포기하고 최종 일관성을 택한 것이다.
- ❌ "2PC는 안전하니 쓰면 된다" → 조정자 장애 시 블로킹이 서비스 전체로 번진다.
두 가지 조율 방식
오케스트레이션(중앙 조율)
- 조율자가 각 단계를 순서대로 호출하고 실패 시 보상을 부른다
- ✅ 흐름이 한곳에 보인다 · 디버깅이 쉽다
- ❌ 조율자가 모든 서비스를 알아야 한다
코레오그래피(이벤트 연쇄)
- 각 서비스가 이벤트를 듣고 다음 이벤트를 낸다
- ✅ 서비스 간 결합이 낮다
- ❌ 전체 흐름이 어디에도 안 적혀 있다 · 추적이 어렵다
단계가 4개를 넘으면 오케스트레이션이 유지보수에 유리하다는 것이 일반적인 경험칙이다.
보상이 실패하면
-
보상 트랜잭션도 실패할 수 있다
-
① 보상은 반드시 멱등하게 만든다 (여러 번 실행돼도 결과가 같게)
-
② 재시도한다
-
③ 그래도 실패하면 사람이 개입할 큐(dead letter)로 보낸다
-
"자동으로 완벽하게 되돌린다" 는 환상이다 — 개입 경로를 반드시 설계한다