🧩 분산 트랜잭션의 복잡성
모놀리식 트랜잭션 vs MSA 트랜잭션
모놀리식의 경우 DB 하나에만 접근하므로 @Transactional 어노테이션 등을 통해 간단하게 트랜잭션을 관리할 수 있고, 모든 데이터가 하나의 DB에만 있기 때문에 커밋/롤백 로직이 단순하다.
반면 MSA의 경우 각각의 서비스가 별도의 DB/메시지 브로커 등을 사용하므로 하나의 비즈니스 로직에 여러 서비스가 연결된다. 전통적인 분산 트랜잭션을 적용하려면 여러 제약이 따르기 때문에 카프카나 RabbitMQ 등에서는 2PC 트랜잭션을 지원하지 않는다고 한다.
[2PC 트랜잭션이란?]
Two-phase Commit: 여러 노드들 상에서의 원자적 트랜잭션 커밋을 이루기 위한 알고리즘. 여러 DB 노드들에 읽고 쓰면서, Phase 1에서 모든 노드가 커밋할 준비가 되었다(yes)라고 하면 Phase 2로 넘어가 Commit 요청을 보내고, 하나라도 no라고 응답하면 Phase 2로 넘어가 모든 노드들에 Abort 요청을 보낸다.
[XA 프로토콜이란?]
eXtended Architecture: 서로 다른 DB나 자원 관리자 간에 분산 트랜잭션을 처리하기 위해 2PC를 기반으로 구성된 기술 표준
XA 프로토콜(이하 전통적 분산 트랜잭션)의 한계
이론적으로는 여러 자원을 동시에 커밋/롤백하는 것이 가능하지만, 참여 노드가 모두 항상 가용 상태에 있어야 하므로 시스템 전체의 가용성을 떨어뜨리는 요인이 된다.
위에서 하나의 노드라도 no라고 응답하면 전체 커밋이 취소된다고 나와 있다. 그런데 여러 DB 서버 중 단 하나의 서버라도 다운되었다거나 네트워크 통신에 문제가 생겼다면 전체 로직 자체가 실패하게 된다. 독립적인 시스템들을 묶어 놓았더니, 전체 시스템의 가용성이 '가장 상태가 안 좋은 노드'의 상태로 하향 평준화되는 결과가 나타나는 것이다.
또한 노드1과 노드2가 yes로 응답한 상태에서 최종 결정이 내려질 때까지 DB 리소스를 Lock으로 잡아서 다른 작업을 수행할 수 없다. 만약 노드3이 네트워크 지연으로 응답이 늦어진다면, 그 데이터에 접근하려는 다른 모든 시스템의 요청들도 무한정 대기해야 한다. 이러한 문제를 동기식 블로킹이라고 한다.
결론적으로 XA 프로토콜은 모든 시스템이 100% 완벽하게 작동하고 통신할 때만 완벽하게 동작하므로, 현실에서 흔히 발생하는 작은 서버 장애나 네트워크 지연 하나만으로도 전체 시스템이 멈추거나 느려지는 족쇄가 된다. 이러한 한계 때문에 최근 MSA에서는 시스템을 멈춰 세우는 XA 프로토콜(2PC)을 거의 쓰지 않는다.
🧩 Saga 패턴의 개념과 필요성
[Saga 패턴이란?]
MSA에서 분산된 DB간의 정합성을 유지하기 위해 여러 서비스의 로컬 트랜잭션을 순차적으로 실행하는 패턴. 하나의 단계가 실패하면 이전 단계들을 역순으로 취소하는 보상 트랜잭션을 통해 최종 일관성을 보장한다.
※ 로컬 트랜잭션: 각 MSA가 자신만의 DB 내에서 완벽하게 보장하는 단일 ACID 트랜잭션
왜 Saga 패턴을 써야 할까?
Saga 패턴은 하나의 거대한 글로벌 트랜잭션으로 모든 서비스의 변경을 포기하는 대신, 하나의 긴 비즈니스 프로세스를 여러 개의 로컬 트랜잭션의 연속적인 실행으로 분리한다.
- 분산 트랜잭션의 대안: 전통적 2PC를 사용하기 어려운 MSA 환경에서, 각 서비스가 로컬 트랜잭션만 책임지고 단계별로 성공/실패 시 이벤트 혹은 Orchestrator(지휘자)를 통해 전체 흐름을 제어하는 방식
- 로컬 트랜잭션 + 보상 트랜잭션: 한 단계가 commit된 후 다음 단계로 넘어갈 때 장애가 생기면 이미 커밋된 작업들을 되돌리는 보상로직으로 정합성을 맞춘다.
Saga의 작동 원리
| T1 | T2 | T3 | T4 | T5 | T6 |
위에서 T1 → T2 → T3가 완료된 이후 T4가 실패했다고 가정하자. 이 경우 역순(T3 → T2 → T1)으로 작업을 취소(Undo)해야 한다. 이를 보상 트랜잭션이라고 한다.
Order (성공) → Payment (성공) → Product (재고 부족으로 실패) 시나리오를 가정해보자. 이 경우 보상 시나리오는 다음과 같다.
- Product 서비스가 실패 이벤트 발행
- 이 이벤트를 받은 Payment 서비스가 "결제 환불" 보상 트랜잭션 실행
- Payment의 환불이 완료되면, 이 이벤트를 받은 Order 서비스가 "주문 취소" 보상 트랜잭션을 실행
이처럼 Saga 패턴은 실패한 지점부터 역순으로, 이미 성공했던 로컬 트랜잭션들을 되돌리는 별도의 보상 트랜잭션을 실행해 데이터를 정리한다.
ACID와의 차이점
MSA에서 트랜잭션이 실행되려면 여러 DB와 서비스 간의 연계가 필요하다. 전통적인 ACID 트랜잭션은 고립성(Isolation)을 보장하지만, Saga에서는 각 단계가 커밋되므로 중간 상태가 외부에 노출될 수 있다.
- 고립성: 1~3번 작업이 완전히 끝날 때까지 다른 사용자나 시스템은 이 작업이 진행 중인 '중간 과정'을 볼 수 없다. (블랙박스 개념이랑 비슷함)
- Product에서 에러가 발생하면 Order와 Payment의 작업은 없던 일로 되어버려(롤백) 밖에서 볼 때는 주문 성공 또는 실패로만 보인다.
- 반면 Saga 패턴에서는 1번, 2번 단계가 끝날 때마다 각자의 DB에 완전히 데이터를 저장(커밋)한다. 따라서 3번 단계가 진행 중인 찰나의 순간에, 사용자가 마이페이지를 새로고침하면 "결제는 완료되었는데 재고 확인 중"이라는 애매한 중간 상태를 보게 되는 것이다.
Saga는 강력한 일관성을 보장하지 않는 대신 최종 일관성(Eventual Consistency)을 보장한다. 즉, Saga가 완료되면 (모든 로컬 트랜잭션 성공 또는 모든 보상 트랜잭션 성공) 시스템의 데이터는 결국 일관된 상태가 된다. 또한 Saga는 강력한 일관성을 포기한 대신 2PC의 치명적인 문제점이었던 동기식 블로킹을 제거한다. 이를 통해 각 서비스는 느슨하게 결합되고 시스템 전체의 가용성과 탄력성을 높일 수 있다.
🧩 오케스트레이션 기반 Saga
[Orchestration이란?]
교향악단 공연에서 각 연주자(MSA)는 다른 연주자가 아니라 중앙의 지휘자(Orchestrator)만을 본다. 지휘자는 각 연주자에게 언제 무엇을 연주할 지 명령(Command)을 내리고, 연주가 끝나면 보고(이벤트)를 받아 다음 악장을 진행한다. 실수가 발생하면 지휘자가 직접 나서서 바로 잡는다.
중앙 집중형 Orchestrator와 Command/Replay
오케스트레이션 기반 Saga에서는 중앙에 Orchestrator가 존재해 모든 로컬 트랜잭션의 순서를 지휘한다. 오케스트레이터가 각 서비스에게 Command 메시지를 보내면 서비스는 작업 완료 후 Reply 이벤트를 되돌려주는 구조다.

- 성공 시나리오
- [Order] 주문이 생성되면 Saga 오케스트레이터에게 "주문 생성 Saga를 시작하라"라고 명령함
- [Orchestrator] Saga 상태를 PENDING으로 저장한 뒤, Payment 서비스에게 ProcessPaymentCommand를 Kafka 등을 통해 보냄
- [Payment] 명령을 받아 결제를 처리하고, 성공하면 PaymentCompletedEvent를 발행함
- [Orchestrator] PaymentCompletedEvent를 구독하고, 다음 단계인 Shipping 서비스에게 ProcessShippingCommand를 보냄
- [Shipping] 명령을 받아 배송을 처리하고, 성공하면 ShippingCompletedEvent를 보냄
- [Orchestrator] ShippingCompletedEvent를 구독하고, 마지막 단계인 Order 서비스에게 ConfirmOrderCommand를 보냄
- [Order] ConfirmOrderCommand를 받고 주문 상태를 최종 업데이트함
- 모든 단계가 성공했으므로 Saga의 최종 상태를 COMPLETED로 변경함
- 실패 시나리오
- 1~4단계 성공
- [Shipping] ProcessShippingCommand를 받았지만 실패 후 ShippingFailedEvent를 보냄
- [Orchestrator] ShippingFailedEvent를 구독하고, Saga 상태를 ROLLING_BACK으로 변경해 보상 트랜잭션을 역순으로 지휘하기 시작함
- [Orchestrator] Payment에게 RefundPaymentCommand를 보냄
- [Orchestrator] Payment로부터 PaymentRefundedEvent를 받으면 Order에게 CancelOrderCommand를 보냄
- 모든 보상 트랜잭션이 완료되면 Saga의 최종 상태를 FAILED로 변경함
오케스트레이션의 장단점
- 장점
- 순환 의존성의 부재: 의존관계가 간단해진다.
- 느슨한 결합: 각 서비스는 Orchestrator의 Command에만 반응하면 되므로 서비스 간 직접 호출이 줄어든다.
- 중앙 집중화된 로직: 전체 비즈니스 프로세스가 오케스트레이터 한 곳에 명시적으로 정의되어, 각 서비스는 로컬 트랜잭션에만 집중하면 되고 Saga의 상태 추적이나 디버깅도 쉽다.
- 단점
- 과도한 책임 집중: 오케스트레이터에 과도한 로직이 쏠리면 오케스트레이션 중심의 모놀리스가 될 위험이 있고, 오케스트레이터 자체가 단일 장애점이 될 수 있다.
- 성능 이슈: 모든 요청이 오케스트레이터를 경유하므로 지연 발생, Throughput 한계가 오케스트레이터 성능에 좌우된다.
- 추가 컴포넌트 관리 부담: 오케스트레이터 개발, 배포, 유지 비용
오케스트레이션 툴
- Netflix Conductor: 넷플릭스에서 오픈소스로 공개한 MSA 오케스트레이션 툴
- Camunda, Zeebe, AWS Step Functions 등...
🧩 코레오그래피 기반 Saga
[Choreography란?]
중앙의 지휘자 없이 각각의 무용수(MSA)가 서로의 춤 동작(이벤트)를 보고 다음에 무엇을 할지 스스로 결정해서 협업하면서 안무(Choreography)가 진행된다.
이벤트 발행/구독 방식
코레오그래피 기반 Saga에서는 오케스트레이터없이 각 서비스가 이벤트를 발행하고 구독해서 스스로 다음 단계를 진행한다.
각 서비스는 자신의 로컬 트랜잭션을 완료한 후, "내가 내 일을 끝냈다"는 사실을 담은 이벤트를 발행한다. 그러면 다음 단계의 책임을 가진 서비스가 이 이벤트를 구독하여 자신의 작업을 시작하는 연쇄 반응이 일어난다.

- 성공 시나리오
- [Order] 주문을 생성하고 commit한 뒤 OrderCreatedEvent를 발행함
- [Payment] OrderCreatedEvent를 구독하여 결제를 처리하고 commit한 뒤 PaymentCompletedEvent를 발행함
- [Shipping] PaymentCompletedEvent를 구독하여 배송을 처리하고 commit한 뒤 ShippingArrangedEvent를 발행함
- [Order] ShippingArrangedEvent를 구독하여 최종적으로 주문 상태를 CONFIRMED로 변경하고 commit함
- 실패 시나리오
- 1, 2번 성공
- [Shipping] PaymentCompletedEvent를 구독했지만 어떤 이유로 인해 로컬 트랜잭션이 실패하면서 ShippingArrangeFailedEvent를 발행함
- [Payment] ShippingArrangedFailedEvent를 구독하고 있다가 이 이벤트를 받고 자신의 보상 트랜잭션을 실행하고 PaymentRefundedEvent를 발행함
- [Order] PaymentRefundedEvent를 구독하고 자신의 보상 트랜잭션을 실행해 주문 상태를 CANCELLED로 변경함

별개로 Outbox 패턴 & 이벤트 방식도 있다.
- 원자성 보장: DB 변경과 이벤트 발행을 하나의 로컬 트랜잭션으로 묶기 위해 Outbox 테이블 기법 사용
- Correlation IDs: 이벤트에 orderId 등 공통 식별자를 포함해 다른 서비스가 어떤 트랜잭션/주문과 연관된 이벤트인지 파악
코레오그래피의 장단점
- 장점
- 느슨한 결합: 서비스들은 서로의 존재를 알 필요 없이 Kafka의 이벤트만 구독하면 된다
- 단순함: 지휘자 컴포넌트가 없어서 구조가 단순하고, 각 서비스는 자신의 책임에만 집중하면 된다.
- 단일 장애점 부재: 중앙 오케스트레이터가 없으므로 한 지점의 장애로 전체가 멈추지 않는다.
- 확장성: 이벤트 기반으로 서비스가 늘어나도 구독만 추가하면 새 기능을 붙이기 비교적 쉽다.
- 단점
- 트랜잭션 가시성 부재: 전체 비즈니스 트랜잭션의 현재 상태를 한눈에 파악하기 여렵고, 각 서비스의 로그와 DB를 뒤져와야 한다.
- 순환 의존성: A가 발행한 이벤트를 B가 구독하고 B가 발행한 이벤트를 A가 구독하는 경우
- 계약 관리 복잡성: 어떤 서비스가 어떤 이벤트를 발행하고 구독하는지를 명시적으로 관리해야 한다.
- 이벤트 플로우 분산: 전체 비즈니스 로직이 여러 서비스 이벤트로 쪼개져 있어 디버깅이 난해해진다.
🧩 오케스트레이션 vs 코레오그래피
| 분류 | 오케스트레이션 | 코레오그래피 |
| 설명 | 순서 제어 및 보상 트랜잭션을 중앙 Orchestrator가 담당 | 이벤트 발행/구독으로 상호작용 |
| 장점 | 서비스 간 결합도 낮음, 전체 흐름 한눈에 파악 쉬움 | 중앙 집중 구성이 없어 장애 전파 위험 감소, 유연성 높음 |
| 단점 | 단일 장애점(SPoF), Orchestrator 로직 과부하 위험 | 이벤트 흐름이 복잡해지면 관리/디버깅 어려움 |
| 추천 | 대규모, 복잡한 흐름 | 소규모, 단순한 흐름, 빠른 확장 |
조직 문화나 시스템 규모에 따라 적절히 선택 또는 혼합해서 사용하면 된다.
- 서비스(도메인) 개수가 늘고 이벤트 흐름 분기가 복잡해진다면 오케스트레이션을 고려해 봐야 한다.
- 코레오그래피는 소규모, 단순한 흐름, 빠른 확장에 용이하지만 복잡도가 커지면 관리가 어려워진다.
무조건 '오케스트레이션이 좋다!' 라는 것은 아니다. 예를 들어, 주문 생성 흐름이 Order → Payment → Shipping 으로 이어지는 비교적 단순한 선형적 흐름이라면 복잡한 분기 로직이 필요하지 않다. 이 경우 오케스트레이션 방식은 과설계가 될 수 있으며, 코레오그래피 방식으로 별도 지휘자 컴포넌트 없이 각 서비스가 Kafka 이벤트를 통해 느슨하게 협업하는 단순유연한 아키텍처가 더 나을 수 있다.
- 서비스 개수: 2~3개의 서비스가 연결되면 코레오그래피를, 5개 이상이면 오케스트레이션을 고려
- 업무 흐름 복잡도: 여러 단계가 순차/병렬적으로 섞여 있고 에러 핸들링 로직이 다양하면 오케스트레이션으로 중앙 집중 제어
- 개발/운영 편의성: 코레오그래피는 이벤트를 잘 이해해야 하고 오케스트레이션은 중앙 집중 로직을 잘 설계해야 함
협업 모델의 관점에서
Saga 패턴 뿐만 아니라 MSA 구조에서도 오케스트레이션과 코레오그래피를 적용할 수 있다.
- 오케스트레이션
- 중앙 Orchestrator가 모든 호출(invoke)와 응답(reply)을 중재
- 서비스 결합도가 낮고 도메인 경계가 명확해 테스트 범위가 분명함
- 중앙 집중 관리로 디버깅이 쉽지만 단일 장애점 및 중앙 로직 과부화(모놀리식화) 우려
- 코레오그래피
- 각 서비스가 직접 메시지를 주고받는 Point-to-Point 통신
- 서비스 의존이 얿혀 복잡해지고 장애 극복 시나리오도 까다로워질 수 있음
🧩 기타 사항
Saga의 관점에서 RabbitMQ vs Kafka?
- RabbitMQ
- 전통적인 메시지 큐 구조: 메시지가 들어오면 큐에서 소비자가 가져가는 방식
- Command 스타일 메시징에 적합함
- 메시지 순서 보장 보다는 일관성에 무게를 두기 쉬움
- 단순한 명령형 메시지 전달과 비교적 낮은 TPS에서는 RabbitMQ가 더 적합함
- Kafka
- 분산 로그 구조: 메시지를 topic에 기록하면 여러 소비자 그룹이 이를 구독하는 방식
- 스트리밍 처리, 대용량 처리, 이벤트 소싱에 강점
- 파티셔닝을 통해 대량의 메시지 처리 가능
- 이벤트 중심, 높은 처리량, 이벤트 소싱 및 CQRS를 고려한다면 Kafka가 더 적합함
Saga 자체는 메시지 브로커 종류에 크게 종속적이지는 않지만, 최종적 일관성 보장을 위한 재처리 메커니즘, 이벤트 중복 처리를 위한 멱등성, 장애 복구 전략 등에 있어 Kafka가 주는 이점이 크다.
보상 트랜잳션이 실패한다면?
- 보상 트랜잭션 실패 시 재처리
- 재시도 정책: 일정 횟수까지만 허용. 초과하면 DLQ나 관리자 개입 필요
- DLQ(Dead Letter Queue) 또는 보상 전용 토픽
- Outbox 패턴: 이벤트를 DB의 Outbox 테이블에 먼저 저장하고 별도 프로세스가 있는 테이블에 있는 이벤트를 카프카로 전달해 발행-커밋 간 불일치를 방지
- 이미 발행된 메시지에 대한 롤백 처리
- 멱등성 보장: 메시지 소비 측에서 동일한 메시지가 와도 로직을 2번 이상 수행하지 않도록 설계
- 보상 메시지 발행: 이전 메시지 취소 대신 상태를 원복하는 새로운 이벤트를 발행해 소비 측에서 취소/보정 로직을 수행
- 트랜잭션 결계 재셜계: 중요한 비즈니스 로직이라면 메시지 발행 시점을 로컬 트랜잭션 커밋과 동기화 또는 Outbox 패턴 고려
- 그래도 계속 실패하면?
- Orchestration + State Machine 관리
- TCC(Try-Confirm/Cancel) 패턴
- 재시도 + DLQ + 모니터링
- 이벤트 소싱 + CQRS (운영 비용 및 복잡성 크게 증가)
- 재처리 전용 엔드포인트 또는 관리자 도구
실무에서 CQRS + Saga를 적용하는 방법
- 비즈니스 임팩트가 큰 핵심 도메인에만 CQRS + Saga를 적용하고, 나머지는 단순 CRUD로 구성하는 혼합 전략
- 무조건 CQRS Saga가 좋다고 아무생각 없이 도입하다가 생산성이 떨어지면 유지보수 비용만 커짐
- 점진적 도입 방법
- 도메인 분리와 DB 분리 (개별 서비스 독립성 일부 확보)
- 이벤트 발행(또는 REST API)으로 서비스 간 의존성을 느슨하게 유지 (필요한 부분부터 Saga 적용)
- CQRS는 필요에 따라 부분 적용 (조회 부하가 많거나 이벤트 소싱이 필요한 경우만)
- 모니터링/테스트 전략 마련
Saga 패턴 테스트는 어떻게 해야 할까?
- 통합 테스트
- Docker 등으로 각 서비스와 Kafka를 띄운 뒤 시나리오 테스트 진행
- 정상 처리 시나리오, 부분 실패 시나리오, 보상 트랜잭션 실패 시나리오 등 모두 넣어야 함
- 계측 및 모니터링: 분산 트랜잭션은 여러 서비스와 큐가 얽히므로 Zipkin 같은 분산 트레이싱 툴로 트랜잭션 경로를 시각화
- CI/CD: 변경사항이 생길 때마다 통합 테스트를 돌려 Saga 시나리오가 깨지지 않는지 확인
실무에서 Saga 패턴 적용 시 발생한 문제와 해결방안
- 메시지 중복 처리 실패: 멱등성 처리가 제대로 안 되어 중복 결제/중복 주문이 발생
- → 트랜잭션 아이디 기반 중복 체크 및 DB Lock/Unique 제약 등으로 해결
- 보상 트랜잭션 무한 실패: 결제 취소 API가 여러 차례 실패해 DLQ로 쌓였는데, 운영에서 이를 놓치는 경우
- → DLQ 모니터링 시스템이나 주기적 알람으로 대응
- Orchestrator 장애: 중앙 오케스트레이터 서비스 다운으로 전체 트랜잭션의 흐름이 중단
- → 고가용성구성 및 장애 시 이벤트 재처리 설계
Saga 패턴을 적용할 때 꼭 고려해야 할 부분은?
- 데이터 멱등성과 중복 처리
- 보상 트랜잭션 설계 (실패 시 어떤 방식으로 어느 단계부터 어떤 순서로 롤백할 것인가?)
- 오케스트레이션 vs 코레오그래피 선택: 도메인 규모, 이벤트 복잡도, 팀 역량에 따라 선택
- 장애 및 예외 상황 모니터링
- 테스트 및 운영 전략
참고자료
- 내일배움캠프 SAGA 세션 학습자료
- https://wikidocs.net/298466
더 읽어보면 좋을 자료들
- https://youtu.be/amTJyIE1wO0
- https://www.youtube.com/watch?v=xpwRTu47fqY
- https://devocean.sk.com/blog/techBoardDetail.do?ID=165445&boardType=techBlog&ref=codenary
'내일배움캠프' 카테고리의 다른 글
| [내일배움캠프] 모니터링 시스템 (0) | 2026.06.02 |
|---|---|
| [내일배움캠프] 로깅 (0) | 2026.05.15 |
| [내일배움캠프] 이벤트 소싱과 CQRS, RabbitMQ와 Kafka (0) | 2026.05.13 |
| [내일배움캠프] 대규모 시스템에서의 DB 최적화와 분산 트랜잭션 (0) | 2026.05.12 |
| [내일배움캠프] Redis 심화 (0) | 2026.05.12 |