티스토리 뷰
분산 트랜잭션에서 이벤트 기반 아키텍처로 전환하기 2편 : MSA Saga 패턴 완벽 이해 - Orchestration vs Choreography 비교와 비동기 메시징 설계
ebson 2026. 3. 11. 21:591편에서 MSA 환경의 분산 트랜잭션 문제와 동기식 해법들의 구조적 한계를 살펴보았습니다. ChainedTransactionManager는 부분 커밋 실패에 대한 보호 장치가 없었고, 2PC는 가용성과 확장성을 희생해야 했습니다. 이번 글에서는 이 문제에 대한 근본적인 대안인 Saga 패턴의 구조를 깊이 들여다보고, 왜 비동기 메시징이 Saga의 표준적인 통신 방식이 되는지 정리합니다.
Saga 패턴이란
Saga 패턴은 하나의 글로벌 트랜잭션 대신, 각 서비스의 로컬 트랜잭션을 순차적으로 연결하여 분산 데이터 정합성을 보장하는 패턴입니다. Microservices.io에서는 Saga를 다음과 같이 정의합니다.
"A saga is a sequence of local transactions. Each local transaction updates the database and publishes a message or event to trigger the next local transaction in the saga."
각 로컬 트랜잭션은 자신의 데이터베이스를 업데이트한 뒤 메시지나 이벤트를 발행하여 다음 서비스의 로컬 트랜잭션을 트리거합니다. 2PC처럼 모든 참여자를 동시에 조율하는 것이 아니라, 각 서비스가 독립적으로 자신의 트랜잭션을 수행하고 그 결과를 전파하는 방식입니다.
이 과정에서 특정 단계가 실패하면 어떻게 될까요? 글로벌 트랜잭션이 아니므로 자동 롤백은 불가능합니다. 대신 Saga는 보상 트랜잭션(Compensating Transaction)이라는 개념을 사용합니다. 실패 이전에 완료된 로컬 트랜잭션들의 변경을 되돌리는 별도의 트랜잭션을 역순으로 실행하여 정합성을 복구합니다.
예를 들어 주문 생성 Saga에서 결제 단계가 실패했다면, 이미 차감된 재고를 복원하는 보상 트랜잭션이 실행됩니다. 데이터베이스 수준의 자동 롤백이 아닌, 비즈니스 로직으로 구현된 역방향 작업인 셈입니다. Microsoft Azure Architecture Center의 Saga 패턴 문서에서는 Saga의 단계를 세 가지 유형으로 분류합니다.
Compensable Transaction은 보상 트랜잭션으로 되돌릴 수 있는 단계입니다. 재고 차감이나 결제 승인 요청처럼, 실패 시 반대 방향의 작업으로 원래 상태를 복구할 수 있는 단계가 여기에 해당합니다.
Pivot Transaction은 Saga의 결정적 분기점입니다. 이 단계가 성공하면 더 이상 보상 트랜잭션으로 되돌아갈 수 없는, 돌이킬 수 없는 지점(point of no return)이 됩니다. Microsoft 문서에서는 이를 "the boundary between reversible and committed"라고 설명합니다. Pivot 이전 단계에서 실패하면 Backward Recovery(보상)를, Pivot 이후에서 실패하면 Forward Recovery(재시도)를 선택하게 됩니다.
Retryable Transaction은 Pivot 이후에 위치하는 단계로, 반드시 완료되어야 하는 작업입니다. Microsoft 문서의 표현을 빌리면 "idempotent and help ensure that the saga can reach its final state, even if temporary failures occur"합니다. 일시적 장애가 발생하더라도 재시도를 통해 최종 상태에 도달할 수 있도록 멱등성이 보장되어야 하는 단계입니다.
이 세 가지 유형의 분류가 중요한 이유는, Saga를 설계할 때 각 단계의 실패에 대한 복구 전략을 미리 결정할 수 있기 때문입니다. 어떤 단계가 Compensable이고 어떤 단계가 Pivot인지를 명확히 정의해야 보상 트랜잭션의 범위와 재시도 전략을 체계적으로 설계할 수 있습니다. 이 주제는 4편에서 더 깊이 다루겠습니다.
Orchestration과 Choreography
Saga 패턴은 구현 방식에 따라 Orchestration과 Choreography 두 가지로 나뉩니다. 이 둘의 핵심 차이는 Saga의 흐름을 누가 제어하느냐에 있습니다.
Orchestration - 중앙 조율자가 흐름을 제어
Orchestration 방식에서는 중앙의 Orchestrator가 Saga의 전체 흐름을 관리합니다. Microservices.io에서는 이를 "an orchestrator tells the participants what local transactions to execute"라고 설명합니다. Orchestrator는 각 서비스에 어떤 로컬 트랜잭션을 실행할지 지시하고, 그 결과에 따라 다음 단계를 결정합니다. AWS Prescriptive Guidance에서도 같은 맥락으로 설명합니다.
Orchestrator는 중앙 코디네이터로서 전체 트랜잭션 라이프사이클을 관리하며, 각 참여 서비스에 메시지를 보내 작업을 수행하도록 요청합니다. 실패가 발생하면 Orchestrator가 보상 트랜잭션의 실행 순서와 대상을 직접 결정합니다. 이 방식의 가장 큰 장점은 Saga 흐름의 가시성입니다. 전체 트랜잭션의 상태가 Orchestrator 한 곳에 집중되므로, 현재 어떤 단계까지 진행되었는지, 실패 시 어떤 보상이 필요한지를 파악하기 용이합니다. Microsoft 문서에서도 Orchestration의 장점으로 "clear separation of responsibilities simplifies service logic"을 꼽습니다. 각 참여 서비스는 자신의 로컬 트랜잭션 로직에만 집중하면 되고, 전체 흐름의 조율은 Orchestrator가 담당합니다.
반면 Orchestrator 자체가 단일 장애점(SPOF)이 될 수 있다는 점은 주의가 필요합니다. AWS 문서에서도 "The orchestrator can become a single point of failure because it coordinates the entire transaction"이라고 명시합니다. Orchestrator에 장애가 발생하면 진행 중인 모든 Saga가 영향을 받을 수 있으므로, Orchestrator의 고가용성 확보가 설계 시 중요한 고려사항이 됩니다.
Choreography - 이벤트 기반의 분산 조율
Choreography 방식에서는 중앙 조율자 없이 각 서비스가 이벤트를 발행하고 구독하여 Saga를 진행합니다. Microservices.io에서는 "each local transaction publishes domain events that trigger local transactions in other services"라고 설명합니다. 각 서비스가 자신의 로컬 트랜잭션을 완료한 후 도메인 이벤트를 발행하면, 이를 구독하고 있는 다음 서비스가 자신의 로컬 트랜잭션을 시작하는 구조입니다.
주문 생성 Saga를 예로 들면, 주문 서비스가 주문을 생성하고 OrderPlaced 이벤트를 발행합니다. 재고 서비스가 이 이벤트를 구독하여 재고를 차감하고 InventoryReserved 이벤트를 발행합니다. 결제 서비스가 이를 받아 결제를 처리하는 식으로, 이벤트의 연쇄가 Saga의 흐름을 구성합니다.
이 방식의 장점은 중앙 조율자가 없으므로 단일 장애점이 존재하지 않는다는 것입니다. 각 서비스가 독립적으로 이벤트를 발행하고 처리하므로 서비스 간 결합도도 낮습니다. Microsoft 문서에서도 Choreography의 장점으로 "doesn't introduce a single point of failure because the responsibilities are distributed across the saga participants"를 언급합니다.
그러나 참여 서비스가 늘어날수록 이벤트 흐름을 추적하기 어려워진다는 단점이 있습니다. 어떤 서비스가 어떤 이벤트에 반응하는지, 실패 시 보상 이벤트의 흐름이 어떻게 되는지를 전체적으로 파악하기가 쉽지 않습니다. AWS Choreography 문서에서도 참여자가 추가될수록 의존성 추적이 어려워진다고 지적합니다.
어떤 방식을 선택할 것인가
두 방식 중 어느 것이 절대적으로 우월한 것은 아닙니다. 시스템의 복잡도와 요구사항에 따라 적합한 방식이 달라집니다.
AWS Prescriptive Guidance에서는 선택 기준에 대한 가이드를 제공합니다. Choreography는 "there are only a few participants in the saga"일 때, 즉 참여 서비스가 적고 단순한 흐름일 때 적합합니다. 중앙 조율자 없이도 이벤트 흐름을 충분히 파악할 수 있을 정도의 복잡도라면, Choreography의 단순함과 높은 장애 격리 능력이 장점이 됩니다.
반면 Orchestration은 "there are many participants, and loose coupling is required between saga participants"일 때 적합합니다. 참여 서비스가 많고 실행 순서가 명확하게 보장되어야 하는 경우, 중앙 Orchestrator가 전체 흐름을 명시적으로 관리하는 것이 복잡도를 효과적으로 제어하는 방법입니다. AWS 문서에서도 "The orchestrator encapsulates the complexity in the logic by
making the participants loosely coupled"라고 설명합니다.
정리하면, 참여 서비스가 2~3개 수준이고 흐름이 단순하다면 Choreography가 자연스러운 선택이 될 수 있습니다. 참여 서비스가 그 이상이거나 분기, 조건부 실행, 복잡한 보상 흐름이 필요하다면 Orchestration이 관리 측면에서 유리합니다.
왜 비동기 메시징인가
Saga 패턴의 구조를 이해했다면, 다음으로 중요한 질문은 서비스 간 통신을 어떤 방식으로 할 것이냐입니다. 각 로컬 트랜잭션의 결과를 다음 서비스에 전달할 때, 동기 HTTP 호출과 비동기 메시징 중 어떤 방식이 적합할까요?
동기 HTTP 호출의 문제점
서비스 간 통신을 동기 HTTP로 구현하면 시간적 결합(Temporal Coupling)이 발생합니다. 호출하는 시점에 대상 서비스가 반드시 가동 중이어야 하며, 대상 서비스에 장애가 발생하면 호출자도 함께 블로킹되거나 실패합니다. 하나의 서비스 장애가 호출 체인을 따라 연쇄적으로 전파될 수 있는 구조입니다.
Saga의 맥락에서 이 문제는 더욱 심각해집니다. Saga는 여러 서비스에 걸쳐 단계적으로 진행되는 흐름이므로, 각 단계의 지연이 전체 응답 시간에 누적됩니다. AWS Prescriptive Guidance의 Saga Choreography 문서에서는 이를 다음과 같이 언급합니다.
"Compensatory transactions can add latency to the overall response time when the saga consists of several steps. If the transactions make synchronous calls, this can increase the latency further."
보상 트랜잭션이 여러 단계로 구성될 때 전체 응답 시간에 지연이 누적되며, 동기 호출을 사용하면 이 지연이 더욱 증가한다는 것입니다. AWS Saga Orchestration 문서에서도 같은 인용문을 제시하며 "Avoid synchronous calls in such cases"라고 명시적으로 권고합니다. 단순히 성능 문제만이 아닙니다. 동기 호출 기반의 Saga에서 중간 단계의 서비스가 다운되면, 이미 완료된 이전 단계의 보상 트랜잭션을 실행할 수도 없게 됩니다. 대상 서비스가 복구될 때까지 Saga 전체가 불확정 상태에 놓이는 것입니다.
비동기 메시징이 해결하는 것
비동기 메시징에서는 서비스가 직접 다른 서비스를 호출하는 대신, 메시지 브로커(예: Kafka, RabbitMQ)에 메시지를 발행합니다. 수신 서비스는 자신의 속도에 맞춰 브로커로부터 메시지를 가져와 처리합니다. 이 구조는 동기 호출의 핵심 문제인 시간적 결합을 제거합니다. 메시지 브로커가 버퍼 역할을 하므로, 발행 시점에 수신 서비스가 가동 중이지 않아도 메시지는 브로커에 안전하게 보관됩니다. 수신 서비스가 복구되면 밀린 메시지를 순서대로 처리할 수 있습니다.
장애 전파도 차단됩니다. 수신 서비스에 장애가 발생해도 발행 서비스는 영향을 받지 않습니다. 각 서비스가 메시지 브로커를 매개로 독립적으로 동작하므로, 서비스별로 독립적인 확장과 배포가 가능해집니다. 이는 MSA가 추구하는 서비스 자율성과 정확히 맞닿는 특성입니다.
Saga의 관점에서 보면, 비동기 메시징은 각 로컬 트랜잭션의 결과를 안정적으로 다음 서비스에 전달하는 인프라가 됩니다. 중간 서비스에 장애가 발생해도 메시지가 유실되지 않으므로, 서비스 복구 후 Saga를 이어서 진행하거나 보상 트랜잭션을 실행할 수 있습니다. 동기 호출에서 발생하던 Saga의 불확정 상태 문제가 구조적으로 해소되는 것입니다.
동기 API와 비동기 Saga의 경계
비동기 메시징이 Saga의 내부 통신에 적합하다는 점은 이해했지만, 실제 시스템에서는 클라이언트가 동기 HTTP 요청으로 비즈니스 작업을 시작하는 것이 일반적입니다. 사용자가 "주문하기" 버튼을 누르면 POST /orders API가 호출되고, 이 동기 요청이 비동기 Saga를 트리거합니다. 여기서 한 가지 설계 과제가 생깁니다.
Microservices.io에서는 이 상황을 다음과 같이 설명합니다.
"A client that initiates the saga, which an asynchronous flow, using a synchronous request (e.g. HTTP POST /orders) needs to be able to determine its outcome."
동기 요청으로 시작된 비동기 Saga의 최종 결과를 클라이언트가 어떻게 알 수 있는가의 문제입니다. Saga는 비동기로 진행되므로, 동기 응답 시점에는 아직 Saga가 완료되지 않았을 수 있습니다. Microservices.io에서는 이에 대한 세 가지 접근을 제시합니다.
첫 번째는 Saga가 완료될 때까지 동기 요청을 대기시키는 방식입니다. 서비스가 OrderApproved나 OrderRejected 같은 최종 이벤트를 수신한 후에야 응답을 반환합니다. 구현이 단순하지만, Saga가 오래 걸리면 클라이언트 타임아웃 문제가 발생할 수 있습니다.
두 번째는 즉시 응답 후 폴링(Polling) 방식입니다. Saga 시작과 동시에 주문 ID 등을 담은 응답을 먼저 반환하고, 클라이언트가 주기적으로 상태를 조회합니다. 서버 부하가 적지만 클라이언트 구현이 복잡해지고, 폴링 간격에 따라 결과 확인이 지연될 수 있습니다.
세 번째는 즉시 응답 후 이벤트 푸시 방식입니다. 역시 먼저 응답을 반환하되, Saga가 완료되면 WebSocket이나 Webhook으로 클라이언트에 결과를 전달합니다. 실시간성이 높지만 WebSocket 연결 관리나 Webhook 엔드포인트 구성이 필요합니다.
어떤 방식이든 핵심은 동기 API와 비동기 Saga의 경계를 명확히 설계하는 것입니다. 클라이언트는 동기 HTTP로 Saga를 시작하지만, 내부적으로는 비동기 메시징으로 Saga가 진행됩니다. 이 두 세계의 접점을 어떻게 처리하느냐가 사용자 경험과 시스템 복잡도를 결정합니다.
비동기 메시징 도입이 가져오는 새로운 과제
비동기 메시징은 Saga의 안정적인 진행을 가능하게 하지만, 새로운 기술적 과제도 함께 가져옵니다. 가장 중요한 과제는 이중 쓰기 문제(Dual Write Problem)입니다. Saga의 각 단계에서 서비스는 자신의 데이터베이스를 업데이트하고 메시지 브로커에 이벤트를 발행해야 합니다. 그런데 데이터베이스와 메시지 브로커는 서로 다른 인프라입니다. 데이터베이스 커밋은 성공했는데 메시지 발행은 실패하면, 다음 서비스는 이전 단계가 완료되었다는 사실을 알 수 없습니다. 반대로 메시지는 발행되었는데 데이터베이스 커밋이 실패하면, 아직 확정되지 않은 변경에 대한 이벤트가 전파됩니다.
Microservices.io에서도 이 문제를 명시적으로 다루며 다음과 같이 설명합니다.
"a service must atomically update its database and publish a message/event. It cannot use the traditional mechanism of a distributed transaction that spans the database and the message broker."
데이터베이스 업데이트와 메시지 발행을 원자적으로 처리해야 하지만, 데이터베이스와 메시지 브로커에 걸친 분산 트랜잭션은 사용할 수 없다는 것입니다. 이 문제를 해결하는 패턴이 바로 Transactional Outbox 패턴이며, 다음 편에서 다루겠습니다.
그 외에도 메시지 순서 보장과 중복 처리 문제가 있습니다. 비동기 메시징에서는 네트워크 장애나 재시도로 인해 메시지가 중복 전달될 수 있으므로, 수신 서비스에서 멱등성(Idempotency)을 보장하는 설계가 필요합니다. 메시지 순서가 보장되지 않으면 Saga의 단계가 잘못된 순서로 실행될 수 있으므로, 메시지 브로커의 파티셔닝 전략과 함께 순서 보장 방안도 고려해야 합니다. 이 주제들은 4편에서 재시도와 에러핸들링을 다루며 함께 정리할 예정입니다.
정리
Saga 패턴은 글로벌 트랜잭션 대신 로컬 트랜잭션의 연쇄와 보상 트랜잭션으로 분산 데이터 정합성을 보장하는 패턴입니다. Orchestration은 중앙 Orchestrator가 전체 흐름을 제어하여 가시성과 관리 편의성이 높지만 SPOF 위험이 있고, Choreography는 이벤트 기반으로 서비스 간 결합도가 낮지만 참여자가 늘어나면 흐름 추적이 어려워집니다.
Saga의 서비스 간 통신은 비동기 메시징이 표준적인 방식입니다. 동기 HTTP 호출은 시간적 결합과 장애 전파라는 구조적 문제를 갖고 있으며, AWS 공식 가이드에서도 다단계 Saga에서의 동기 호출을 지양하도록 권고합니다. 비동기 메시징은 메시지 브로커를 매개로 시간적 결합을 제거하고 장애 전파를 차단하여, MSA의 독립성과 확장성을 유지하면서 Saga를 안정적으로 진행할 수 있게 합니다.
다음 편에서는 비동기 메시징 도입 시 가장 먼저 마주치는 이중 쓰기 문제와, 이를 해결하는 Transactional Outbox 패턴을 살펴보겠습니다.
References
- Chris Richardson, Microservices Patterns (Manning, 2018) — Chapter 4: Managing transactions with sagas
- Microservices.io — Saga pattern: https://microservices.io/patterns/data/saga.html
- Microsoft Azure Architecture Center — Saga distributed transactions pattern: https://learn.microsoft.com/en-us/azure/architecture/reference-architectures/saga/saga
- AWS Prescriptive Guidance — Saga pattern: https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/saga.html
- AWS Prescriptive Guidance — Saga choreography: https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/saga-choreography.html
- AWS Prescriptive Guidance — Saga orchestration: https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/saga-orchestration.html
'STUDY' 카테고리의 다른 글
- Total
- Today
- Yesterday
- Spring Batch
- Cache Avalanche
- Redis vs DB
- Redis 캐시 전략
- Eager Initialization
- 캐시 성능 비교
- InterruptedException
- 동시성처리
- DB 인덱스 성능
- TTL 설계
- 백엔드 아키텍처
- Hot Key 문제
- spring batch 5
- Java Performance
- DB 트랜잭션
- 트랜잭션 관리
- Redis 성능 개선
- 스레드 생명주기
- 캐시 장애
- 캐시와 인덱스
- 백엔드 성능
- Enum 기반 싱글톤
- 백엔드 성능 튜닝
- 트래픽 처리
- Cache Aside
- Initialization-on-Demand Holder Idiom
- 백엔드 성능 설계
- Double-Checked Locking
- Cache Penetration
- mybatis
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |

