Distributed Transactions & Saga Dynamics
여러 서비스에 걸친 원자적 연산을 보장하는 2단계 커밋과, 긴 실행 시간의 비즈니스 정합성을 관리하는 사가(Saga) 패턴의 물리 메커니즘을 다루는 학습 노드입니다.
Article
M
Me
hyunyoun's Blog
system-architecture-distributed-systemssystem-architecturedistributed-systemsdistributed-systems-principlesconsensusdistributed-transactionssaga-dynamicslearning10 min read
1. Overview
분산 트랜잭션과 사가 역학(Distributed Transactions & Saga Dynamics)은 쪼개진 여러 개의 마이크로서비스(독립된 DB들)에 걸쳐 발생하는 비즈니스 프로세스(예: 결제 재고 차감 배송)를, 장애 상황에서도 완벽히 롤백(Rollback) 시키거나 끝까지 멱등하게 완료시키는 궁극의 데이터 정합성 패턴입니다.
학습자는 모놀리스 시대의 완벽했던 락(Lock) 기반 **2PC(Two-Phase Commit)**가 마이크로서비스 환경에서 왜 네트워크 지연과 스레드 고갈의 원흉(안티 패턴)으로 전락했는지 해부합니다. 이를 극복하기 위해, 거대한 트랜잭션을 개별 서비스들의 작은 트랜잭션 릴레이로 쪼개고 장애 시 '역방향 보상 트랜잭션(Compensating Transaction)'을 발행하는 **사가 패턴(Saga Pattern)**을 뜯어봅니다. 마지막으로, 사가 흐름을 통제하는 두 가지 설계 방식인 **오케스트레이션(Orchestration, 중앙 통제)**과 **코레오그래피(Choreography, 이벤트 안무)**의 물리적 트레이드오프를 장악하여 엔터프라이즈 결제망을 설계하는 역량을 확보합니다.
2. Scope & Boundaries
In-Scope
- 2PC (Two-Phase Commit): 중앙 코디네이터를 통한 Prepare - Commit 2단계 분산 락킹 기법의 한계.
- Saga Pattern (사가 패턴): 로컬 트랜잭션의 연속적인 체인(Chain)과 보상 트랜잭션(Compensation).
- Orchestration (오케스트레이션): 중앙 컨트롤러(Orchestrator)가 각 서비스에 명령을 내리는 동기적/순차적 통제.
- Choreography (코레오그래피): 댄서들이 각자 이벤트(Kafka 등)를 듣고 비동기적으로 반응하는 탈중앙화 릴레이.
- Outbox Pattern: DB 쓰기와 메시지 큐 발행의 원자성을 보장하는 무결성 패턴.
Out-of-Scope
- 이벤트 소싱(Event Sourcing) 및 CQRS: 쓰기/읽기 모델 분리와 이벤트 스트림 영속성 06-02-06 Data Modeling for Connected Data 영역으로 위임 (사가는 트랜잭션 '실행과 복구'에 집중함).
- 단일 DB 트랜잭션 및 격리 수준(Isolation): 06-01-03 ACID & Transactions 영역.
Boundaries
- Locking(2PC) vs Compensation(Saga): 2PC는 "모두가 준비(Prepare)될 때까지 아무도 DB 락(Lock)을 풀지 마!"라고 강제합니다. 이는 완벽한 격리(Isolation)를 보장하지만 락킹 시간이 네트워크 지연에 비례하여 길어져 성능이 박살납니다. 반면 Saga 패턴은 "일단 각자 로컬 DB에 커밋(Commit)해버려. 만약 나중에 뒤쪽 서비스에서 에러 나면, 앞에 커밋했던 애들한테 '취소(보상)' 이벤트를 날려서 데이터를 덮어쓰자"라는 철학입니다. 사가는 락을 걸지 않아 압도적으로 빠르지만, 중간 단계의 데이터가 잠깐 다른 유저에게 노출되는 '격리성 위반(Lack of Isolation)'의 대가를 비즈니스 레벨(상태값:
PENDING)에서 짊어져야 하는 트레이드오프의 극단입니다.
3. Counterexample
- 2PC로 MSA 결제망 구축하기: 마이크로서비스 구조에서 결제, 재고, 쿠폰 3개의 DB를 묶어 2PC(Two-Phase Commit) 글로벌 트랜잭션을 걸었습니다. 재고 DB가 잠시 네트워크 딜레이(3초)를 겪자, 그 3초 동안 결제 DB와 쿠폰 DB의 락(Lock)도 풀리지 않은 채 대기합니다. 블랙프라이데이 트래픽이 몰리자 전체 시스템의 커넥션 풀이 10초 만에 바닥나고 전사 서비스가 마비되는 MSA의 금기(Taboo)를 어긴 참사입니다.
- 보상 트랜잭션 없는 분산 호출 (The Zombie Data): 유저가 주문을 합니다. 주문 서버가 DB에 '주문 생성'을 커밋(Commit)한 직후, 배송 서버로 API를 날렸는데 타임아웃이 터졌습니다. 개발자는 "에러 났네" 하고 유저에게 결제 실패 화면을 띄워버렸지만, 정작 주문 DB에는 주문 내역이 영원히 찌꺼기(Zombie)로 남아있습니다. 뒤쪽 프로세스가 실패했을 때 앞쪽 DB를 롤백(Rollback)하는 로직을 사가(Saga) 패턴으로 설계하지 않아 낳은 데이터 정합성 붕괴입니다.
4. Prerequisites
- 트랜잭션 ACID (Basic): 원자성(Atomicity)과 격리성(Isolation)의 개념. (06-01-03 ACID)
- 메시지 큐 통신 (Basic): Pub/Sub 모델과 이벤트 비동기 통신. (07-01-04 Design Patterns)
5. Learning Map
6. Learning Topics
Basic
Core Topic 01: 락(Lock)의 지옥, 2PC의 몰락 (The Death of 2PC)
- Why to Learn: MSA 환경에서 왜 XA 트랜잭션이나 글로벌 트랜잭션(2PC) 매니저를 도입하면 안 되는지, 그 성능적/구조적 한계를 뼛속까지 인지하기 위함입니다.
- What to Learn:
- Concepts: 2PC (Two-Phase Commit), Prepare Phase, Commit Phase, Coordinator (중앙 통제자), Distributed Lock.
- Skills: 분산 락 유지 시간(Lock Duration)으로 인한 병목 지점 계산.
- How to Learn:
- 1단계: Prepare 단계의 고문: 코디네이터가 DB_A와 DB_B에게 "준비됐냐?"라고 묻습니다. DB_A와 DB_B는 해당 로우(Row)에 락을 걸고 "준비됨(ACK)"을 보냅니다.
- 2단계: 무한 대기: 코디네이터가 DB_A의 ACK는 받았는데, 네트워크 단절로 DB_B의 ACK를 30초째 못 받고 있습니다. 이때 끔찍한 일은, DB_A는 코디네이터의 "Commit 해라" 혹은 "Rollback 해라"라는 최종 명령이 올 때까지 30초 내내 락(Lock)을 쥐고 다른 모든 요청을 다 튕겨내는(Blocking) 인프라 마비 사태를 해부합니다.
- Implement: 2PC 락 블로킹 시뮬레이션 코드. 파이썬 스레드로
Coordinator,DB_A,DB_B실행.DB_B에서 고의로sleep(10)지연 발생.DB_A가 락을 해제하지 못해, 다른 유저 스레드가DB_A를 읽으려다Timeout Error가 연쇄 폭발(Cascading Failure)하는 터미널 콘솔 데모.
Recommended
Core Topic 02: 뒤로 걷는 마법, 사가 패턴과 보상 (Saga & Compensation)
- Why to Learn: 락을 걸지 않고 독립적으로 커밋(Commit)해버리는 마이크로서비스 환경에서, 중간에 실패가 났을 때 이미 커밋된 과거의 DB를 롤백(Rollback)하는 논리적 패턴을 장악하기 위함입니다.
- What to Learn:
- Concepts: Saga Pattern, 로컬 트랜잭션(Local Transaction), 보상 트랜잭션(Compensating Transaction), 격리성 결여 (Lack of Isolation).
- Skills: 비즈니스 워크플로우에 따른 정방향 트랜잭션과 역방향(보상) 트랜잭션 쌍(Pair) 설계.
- How to Learn:
- 1단계: 개별 커밋: [주문 서비스 재고 서비스 결제 서비스]. 주문이 자기 DB에 커밋(락 해제). 재고가 자기 DB 차감 커밋(락 해제). 결제가 돈을 빼려는데 잔액 부족으로 실패합니다.
- 2단계: 보상 트랜잭션(되감기): 결제가 실패하면, 재고 서비스에 "아까 깎은 재고 다시 돌려놔(
+1)"라는 보상 이벤트를 쏩니다. 재고가 복구되면, 주문 서비스에 "주문 상태를 '취소'로 바꿔라"라는 보상 이벤트를 쏩니다. DB 레벨의 물리적 롤백이 아닌, 어플리케이션 레벨의 논리적 덮어쓰기 복구 메커니즘을 해부합니다.
- Implement: 상태 머신(State Machine) 형태의 보상 트랜잭션 로직 구현.
Order(SUCCESS) -> Inventory(SUCCESS) -> Payment(FAIL). 예외catch블록에서Inventory.compensate()와Order.compensate()를 역순으로 호출하여 데이터 정합성을 수동으로 맞춰내는 롤백 릴레이 스크립트.
Practical
Core Topic 03: 지휘자와 댄서, 오케스트레이션 vs 코레오그래피 (Orchestration vs Choreography)
- Why to Learn: 사가 패턴을 구현할 때, 중앙에 통제실(Orchestrator)을 둘 것인가 아니면 큐(Kafka)를 통해 각자 자율적으로 이벤트를 주고받게 할 것인가, 두 가지 아키텍처의 트레이드오프를 저울질하기 위함입니다.
- What to Learn:
- Concepts: Orchestration(중앙 지휘자 패턴), Choreography(탈중앙화 무용수 패턴), Event Broker(이벤트 브로커), Smart Controller vs Smart Endpoints.
- Skills: 서비스 복잡도(단계 수)에 따른 사가 제어 모델 취사선택.
- How to Learn:
- 1단계: 코레오그래피(자율성): 주문 서비스가 "주문 생성됨" 이벤트를 Kafka에 던지고 끝. 재고 서비스가 그걸 주워 듣고 재고를 깎은 뒤 "재고 차감됨" 이벤트를 던짐. 중앙 통제가 없어 빠르고 결합도가 낮지만, 프로세스가 5단계를 넘어가면 "도대체 지금 결제가 어디서 막힌 거야?" 하고 추적(Tracing)이 불가능해지는 지옥을 해부합니다.
- 2단계: 오케스트레이션(통제력):
Order_Saga_Manager라는 놈이 중앙에 앉아 "야 재고 깎아 깎았습니다 그럼 이제 결제해 실패했습니다 그럼 다시 재고 원복해"라고 일일이 명령(Command)을 내립니다. 로직이 중앙에 모여 파악하기는 쉽지만, Orchestrator 자체가 과거 ESB 같은 병목 덩어리가 될 위험을 뜯어봅니다.
- Implement: 두 가지 방식의 구조도 스크립트 모델링. 1)
Choreography: 각 서비스가pub/sub만 호출하는 Event-driven 함수 체인. 2)Orchestration:while(state != DONE)루프 안에서 if-else로 모든 흐름과 보상(Compensate)을 제어하는 중앙 집중형 클래스 설계 차이 렌더링.
Advanced
Core Topic 04: 완벽한 발송, 트랜잭셔널 아웃박스 패턴 (Transactional Outbox Pattern)
- Why to Learn: MSA 통신의 가장 흔한 버그인 "DB에는 저장이 됐는데 Kafka로 이벤트를 쏘기 직전에 서버가 죽어버려 후속 서비스가 영원히 이벤트를 못 받는" 원자성 파괴 참사를 물리적으로 차단하기 위함입니다.
- What to Learn:
- Concepts: Transactional Outbox Pattern, Message Relay, CDC (Change Data Capture), Debezium, 멱등성(Idempotency).
- Skills: RDBMS 로컬 트랜잭션 락을 이용한 비즈니스 테이블과 아웃박스 테이블 동시 쓰기(Two-table Commit).
- How to Learn:
- 1단계: Dual Write의 위험성:
save_to_db(order)호출 성공! 그리고 0.001초 뒤send_to_kafka(order_event)를 호출하려는데 그 찰나에 서버 코드가 크래시 났습니다. DB엔 주문이 찍혔는데, 배송 서버는 이벤트를 못 받아 유저는 영원히 물건을 못 받는 파국을 해부합니다. - 2단계: Outbox 테이블로 묶기:
save_to_db를 할 때, 같은 로컬 트랜잭션 안에서 큐에 보낼 메시지를Outbox테이블에 같이INSERT해버립니다(이 둘은 같은 DB라 100% 원자성이 보장됨). 그 후 별도의 폴러(Poller) 프로세스나 CDC 도구(Debezium)가Outbox테이블을 읽어서 안전하게 Kafka로 쏘는 딜리버리 인프라를 뜯어봅니다.
- 1단계: Dual Write의 위험성:
- Implement: Outbox 패턴 시뮬레이션. SQL 스크립트 수준에서
BEGIN TRANSACTION; INSERT INTO order ...; INSERT INTO outbox_event ...; COMMIT;원자적 묶음을 구현. 이후 백그라운드 스레드가SELECT * FROM outbox_event WHERE sent=false를 폴링(Polling)하여 메시지를 발송하고sent=true로 마킹하는 안정적인 비동기 이벤트 발행 파이프라인 데모.
7. Terminology
8. References
Primary
- [P1] CS2023 - Software Engineering (SE) - Software Architecture (Distributed Transactions)
- [P5] SFIA - Enterprise IT Architecture (ARCH) - System Integration
Secondary
- [Microservices Patterns] Chris Richardson - Sagas, Orchestration vs Choreography, and Outbox Pattern
- [Enterprise Integration Patterns] Gregor Hohpe - Message Routing and Transformation
Industry
- [Uber Engineering Blog] - Distributed Tracing and Saga Patterns at Uber
- [Debezium Documentation] - Change Data Capture (CDC) and the Outbox Pattern
9. Final Checklist
Primary
- 모놀리식 단일 DB 환경에서 완벽하게 동작하던 커밋/롤백(ACID) 모델을 MSA 환경에 그대로 2PC로 가져왔을 때 발생하는 락(Lock) 경합과 타임아웃 붕괴를 물리적으로 증명할 수 있는가?
- 사가(Saga) 패턴을 설계할 때 정방향 로컬 트랜잭션(예: 결제 완료)에 반드시 로 매칭되는 보상 트랜잭션(예: 결제 취소) API를 구현해야 하는 이유를 설명할 수 있는가?
Secondary
- 서비스 개수가 3~4개일 때 유용한 코레오그래피(Choreography) 방식이 서비스 10개가 넘어가면 왜 '디버깅 지옥'으로 변하며 오케스트레이션(Orchestration)으로 회귀해야 하는지 논증할 수 있는가?
- 사가 패턴의 치명적 단점인 '격리성 결여(Lack of Isolation)'로 인해, 결제는 취소(보상 트랜잭션 진행 중)되고 있는데 다른 유저가 재고를 훔쳐 읽는(Dirty Read) 현상의 비즈니스 리스크를 해부할 수 있는가?
Industry
- "DB 쓰기"와 "Kafka 이벤트 발행"을 동시에 실행하는 듀얼 라이트(Dual Write) 시나리오에서 서버가 크래시될 경우 발생하는 영구적 데이터 불일치를 식별할 수 있는가?
- 트랜잭셔널 아웃박스(Outbox) 패턴과 Debezium 같은 CDC(Change Data Capture) 도구를 결합하여, 애플리케이션 코드가 Kafka 통신 에러에 전혀 영향을 받지 않도록 인프라를 격리(Decoupling) 시킬 수 있는가?