본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Backend

분산 트랜잭션: 2PC와 사가

작성 2026. 11. 1.추가
목차9개 항목
출처와 참고

「fru1tworld/cs-basic 모의 면접 질문 목록」의 분산 트랜잭션·사가 항목이 묻는 개념을 바탕으로, Chris Richardson의 「Pattern: Saga」를 참고해 재구성한 노트다. karanpratapsingh의 「System Design」 강의 저장소를 바탕으로 3단계 커밋의 단계와 한계, 사가의 디버깅·테스트 어려움을 보강했다.

분산 트랜잭션이란

분산 트랜잭션(distributed transaction) 은 서로 다른 저장소 여러 곳에 걸친 쓰기를 전부 반영하거나 전부 취소하는 트랜잭션이다. 주문 생성(주문 DB), 결제 승인(결제 DB), 재고 차감(재고 DB)이 한 업무인데 저장소가 셋이면, 한 DB의 로컬 트랜잭션(트랜잭션과 회복)은 자기 DB만 원자적으로 만들 뿐 나머지를 묶지 못한다.

필요해지는 때는 다음과 같다.

방법은 크게 둘이다. 모든 참여자가 함께 확정하는 2단계 커밋과, 각자 로컬 트랜잭션으로 진행하고 실패하면 되돌리는 사가다. DB 쓰기와 이벤트 발행만 묶는 경우는 둘 다 필요 없고 트랜잭셔널 아웃박스로 푼다(데이터 관리 패턴).

2단계 커밋(2PC)

2단계 커밋(two-phase commit) 은 코디네이터(coordinator) 하나가 참여자(participant)들의 커밋을 두 단계로 조율하는 프로토콜이다. 표준 인터페이스가 XA이고, PostgreSQL의 PREPARE TRANSACTION, MySQL의 XA START … XA PREPARE … XA COMMIT이 참여자 쪽 구현이다.

sequenceDiagram
  participant co as 코디네이터
  participant p as 참여자 A, B, C
  co->>p: 1단계: PREPARE
  Note over p: 작업을 디스크에 기록하고 잠금을 쥔 채<br/>「커밋할 수 있다」고 약속(투표)
  p-->>co: YES / NO
  Note over co: 결정을 자기 로그에 기록<br/>모두 YES → COMMIT, 하나라도 NO·무응답 → ABORT
  co->>p: 2단계: COMMIT / ABORT
  Note over p: 결정대로 확정하고 잠금을 푼다
  p-->>co: ACK

YES로 투표한 참여자는 그 뒤로 혼자서는 커밋도 취소도 할 수 없다. 코디네이터의 결정을 기다려야 한다. 이 약속 덕분에 원자성이 지켜진다.

2PC가 잘 쓰이지 않는 이유

문제내용
블로킹참여자가 YES를 보낸 뒤 코디네이터가 죽으면, 참여자는 결정을 모른 채 잠금을 쥐고 기다린다(in-doubt 상태). 그 행을 건드리는 다른 트랜잭션도 함께 막힌다
코디네이터 단일 장애점코디네이터가 복구돼 로그에서 결정을 읽어야 끝난다. 결정 로그를 잃으면 사람이 수동으로 정리한다
지연과 처리량왕복 두 번과 참여자마다의 디스크 기록이 더해지고, 그동안 잠금을 쥐므로 경합이 늘어난다
가용성참여자 전원이 살아 있어야 커밋한다. 참여자가 늘수록 성공 확률은 곱으로 떨어진다
지원 범위참여자 모두 XA를 지원해야 한다. 대부분의 메시지 브로커, NoSQL, 외부 REST API는 참여할 수 없다
결합서비스들이 같은 트랜잭션 관리자와 잠금 수명에 묶여 독립 배포·확장이라는 마이크로서비스의 목적과 어긋난다

3단계 커밋(3PC) 은 2PC의 커밋 단계를 둘로 나눠 블로킹을 줄이려 한다.

단계하는 일
1. 준비(prepare, can-commit)2PC의 1단계와 같다. 모두 YES여야 다음으로 간다
2. 사전 커밋(pre-commit)코디네이터가 「모두 준비됐다」는 사실을 알리고 참여자 전원의 확인을 받는다. 정해진 시간 안에 이 메시지를 못 받은 참여자는 중단한다
3. 커밋(do-commit)2PC의 2단계처럼 확정한다

사전 커밋을 받은 참여자는 모든 참여자가 1단계를 끝냈다는 것을 안다. 그래서 그 뒤에 코디네이터가 죽어도 참여자끼리 타임아웃 후 커밋으로 진행할 수 있고, 단계마다 타임아웃이 있어 무한정 기다리지 않는다. 하지만 이 추론은 메시지 지연에 상한이 있다고 가정하므로, 네트워크 분할로 일부는 사전 커밋을 받고 일부는 못 받으면 양쪽이 서로 다른 결정을 내릴 수 있다. 왕복도 한 번 더 늘어 실제로는 거의 쓰이지 않는다. 대신 분산 DB는 참여자 각각을 합의(Paxos·Raft)로 복제한 그룹으로 만들어, 참여자가 「죽지 않는」 상태에서 2PC를 돈다(Google Spanner). 샤딩 미들웨어 중에도 여러 노드에 걸친 쓰기를 내부에서 2PC로 처리하는 것이 있다(Citus 등). 결국 2PC는 한 DB 제품 내부에서는 쓰이지만, 서비스 사이 연동에는 사가를 쓴다.

사가 패턴

사가(saga) 는 긴 업무를 서비스별 로컬 트랜잭션의 연쇄로 나누고, 중간 단계가 실패하면 이미 끝난 단계를 보상 트랜잭션(compensating transaction) 으로 거꾸로 되돌리는 패턴이다. 1987년 DB 논문에서 오래 걸리는 트랜잭션(long-lived transaction)을 위해 나온 개념이다.

정상:

flowchart LR
  t1["T1 주문 생성(PENDING)"] --> t2["T2 결제 승인"] --> t3["T3 재고 차감"] --> t4["T4 주문 확정(APPROVED)"]

실패:

flowchart LR
  t1["T1"] --> t2["T2"] --> t3["T3 실패"] --> c2["C2 결제 취소(환불)"] --> c1["C1 주문 거절(REJECTED)"]
비교2PC사가
원자성모두 함께 확정결국 모두 반영되거나 모두 보상된다
격리잠금으로 보장없다. 중간 결과가 다른 트랜잭션에 보인다
잠금전체가 끝날 때까지 쥔다단계마다 로컬 트랜잭션이 끝나면 푼다
일관성즉시최종 일관성
실패 처리프로토콜이 롤백보상 로직을 업무 규칙으로 직접 설계
참여자 요구XA 지원로컬 트랜잭션과 메시지·API만 있으면 된다
맞는 곳한 DB 제품 내부, 짧은 트랜잭션서비스 사이, 오래 걸리는 업무

ACID의 A·C·I·D 중 사가는 격리(I)를 포기한 대가로 가용성과 독립성을 얻는다.

보상 트랜잭션 설계

보상은 DB 롤백이 아니라 업무상 반대 동작(semantic undo) 이다. 결제의 보상은 「결제 행 삭제」가 아니라 「환불」이고, 기록에는 결제와 환불이 둘 다 남는다.

원칙이유
멱등하게보상 요청도 재전송된다. 두 번 환불하면 안 된다. 사가 ID·단계 ID로 처리 여부를 기록한다
반드시 성공하게(재시도 가능하게)보상이 실패하면 데이터가 어긋난 채 남는다. 업무 규칙상 거절될 수 없는 동작으로 만든다
순서 뒤바뀜에 견디게원 요청보다 보상 요청이 먼저 도착할 수 있다(원 요청이 타임아웃 났는데 실제로는 늦게 처리되는 경우). 「취소된 사가 ID」를 기록해 늦게 온 원 요청을 거부한다
되돌릴 수 없는 일은 뒤로이메일 발송, 외부 송금처럼 보상이 불가능한 동작은 사가의 뒤쪽에 둔다

이 마지막 원칙을 단계 배치로 정리하면 다음과 같다.

단계 종류뜻놓는 자리
보상 가능 트랜잭션(compensatable)뒤에서 실패하면 보상으로 되돌린다앞쪽
피벗 트랜잭션(pivot)이것이 성공하면 사가는 반드시 끝까지 간다. 되돌리지 않는 분기점가운데 하나
재시도 가능 트랜잭션(retriable)피벗 뒤의 단계. 실패해도 보상하지 않고 성공할 때까지 재시도한다뒤쪽

보상이 실패하면

보상은 「성공할 때까지」가 원칙이다.

  1. 재시도: 지수 백오프 + 지터로 다시 시도한다. 그러려면 보상이 멱등해야 한다(장애 확산을 막는 복원력 패턴).
  2. 상태 영속화: 사가가 어느 단계까지 갔고 무엇을 보상했는지 사가 로그(오케스트레이터 DB)에 기록해, 프로세스가 죽어도 이어서 보상한다.
  3. 격리와 경보: 재시도 상한을 넘으면 DLQ나 「수동 처리 대기」 상태로 옮기고 운영자에게 알린다. 사가는 COMPENSATION_FAILED 같은 상태로 남겨 다른 흐름이 그 데이터를 정상으로 쓰지 않게 한다.
  4. 사후 대사(reconciliation): 배치가 서비스들의 기록을 맞대어 어긋난 건을 찾고 보정한다. 결제처럼 돈이 걸린 곳은 이 안전망을 반드시 둔다(결제 시스템 설계: 흐름, 멱등성, 대사, 핫스팟 계정).

중간 상태와 격리 부재

사가가 진행되는 동안에는 일부 단계만 반영된 상태가 외부에 보인다. 주문은 만들어졌는데 결제는 아직 진행 중인 상태가 그렇다. 격리가 없어서 생기는 이상은 셋이다.

이상예
갱신 손실(lost update)사가가 고친 값을 다른 사가가 읽지 않고 덮어쓴다
더티 리드(dirty read)나중에 보상될 값을 다른 트랜잭션이 읽고 판단한다. 취소될 결제로 등급을 올려 준다
퍼지 리드(fuzzy read)한 사가가 같은 데이터를 두 번 읽는 사이 다른 사가가 바꿔 값이 다르다
대응책동작
시맨틱 락(semantic lock)보상 가능 단계가 레코드에 PENDING·APPROVAL_PENDING 같은 상태를 표시한다. 다른 요청은 이 상태를 보고 거절하거나 기다리거나 사용자에게 알린다. 가장 흔한 방법이다
교환 가능한 갱신(commutative update)「잔액 = X」 대신 「+100, −100」처럼 순서와 상관없이 같은 결과가 나오는 연산으로 만든다
비관적 관점(pessimistic view)단계 순서를 바꿔 더티 리드의 위험이 큰 갱신을 재시도 가능 단계(뒤쪽)로 옮긴다
값 다시 읽기(reread value)갱신 직전에 값을 다시 읽어 바뀌었으면 사가를 중단하고 다시 시작한다(낙관적 잠금)
버전 파일(version file)순서가 뒤바뀐 요청을 기록해 두고 올바른 순서로 재배열해 적용한다
값에 따라(by value)금액이 크거나 위험한 요청만 2PC·강한 일관성 경로로 보내고 나머지는 사가로 처리한다

사용자 경험도 중간 상태를 전제로 설계한다.

  • 화면에 「주문 접수됨, 결제 확인 중」처럼 진행 중 상태를 그대로 보여 주고, 끝나면 알림·폴링·웹소켓으로 갱신한다. 「완료」라고 먼저 말하고 나중에 취소하는 것보다 낫다.
  • 재고·좌석처럼 경합하는 자원은 임시 예약(만료 시간 포함) 으로 잡고, 사가가 끝나면 확정하거나 만료로 푼다.
  • 중간 상태 레코드는 다른 기능(정산, 추천, 통계)이 확정 데이터로 쓰지 않게 상태로 거른다.

구현: 코레오그래피와 오케스트레이션

비교코레오그래피(choreography) 사가오케스트레이션(orchestration) 사가
진행각 서비스가 앞 단계의 이벤트를 구독해 자기 단계를 하고 다음 이벤트를 낸다오케스트레이터가 단계마다 명령을 보내고 응답을 받아 다음을 정한다
사가 상태이벤트에 흩어져 있다. 「지금 어디까지 갔나」를 한곳에서 볼 수 없다오케스트레이터의 상태 기계·DB에 있다
보상실패 이벤트를 각 서비스가 구독해 각자 보상한다. 단계가 늘면 누가 무엇에 반응하는지 추적하기 어렵다오케스트레이터가 역순으로 보상 명령을 보낸다
결합서비스끼리 서로의 이벤트를 안다. 순환 의존이 생기기 쉽다서비스는 오케스트레이터의 명령만 안다. 오케스트레이터에 로직이 몰린다
맞는 곳단계가 2~4개인 단순한 흐름단계가 많고, 분기·타임아웃·보상이 복잡한 흐름

사가는 운영도 어렵다. 여러 서비스에 흩어진 흐름이라 디버깅이 특히 어렵고(분산 추적과 사가 ID 로그가 필요하다), 한 흐름을 시험하려면 관련 서비스를 모두 띄워야 해 테스트가 어렵다. 참여자 사이에 격리가 없어 다른 사가가 중간 상태를 읽고 덮어쓸 수 있다(위 「중간 상태와 격리 부재」).

오케스트레이터는 Temporal, Camunda, AWS Step Functions 같은 워크플로 엔진으로 만들면 상태 영속화·재시도·타이머를 엔진이 맡는다. 두 방식의 일반 비교(신뢰성, 변경 비용, 성능)는 마이크로서비스 아키텍처에 있다. 어느 쪽이든 단계 사이의 메시지는 아웃박스로 발행해야 「로컬 커밋은 됐는데 다음 단계 메시지가 사라지는」 일이 없다.

정리

  • 저장소가 여럿이면 로컬 트랜잭션으로는 원자성을 못 얻는다. 2PC로 함께 확정하거나 사가로 진행·보상한다.
  • 2PC는 원자성과 격리를 주지만 블로킹, 코디네이터 장애, 전원 생존 요구 때문에 서비스 사이에는 맞지 않는다.
  • 사가는 격리를 포기하고 로컬 트랜잭션 + 보상으로 최종 일관성을 얻는다. 보상은 멱등하고 반드시 성공해야 하며, 되돌릴 수 없는 단계는 피벗 뒤로 둔다.
  • 보상 실패는 재시도, 사가 로그, 경보·수동 처리, 대사 배치로 다룬다.
  • 중간 상태는 시맨틱 락과 진행 중 UX로 드러내 다루고, 복잡한 흐름은 오케스트레이션으로 만든다.

관련 노트: 데이터 관리 패턴: CDC, 이벤트 소싱, CQRS, 최종 일관성, 마이크로서비스 아키텍처, 분산 시스템의 통신·데이터 흐름 패턴, 이벤트 기반 아키텍처 설계, 결제 시스템 설계: 흐름, 멱등성, 대사, 핫스팟 계정, 트랜잭션과 회복

원본 기록3

댓글

GitHub 계정으로 댓글을 남길 수 있어요.