티스토리 뷰
분산 트랜잭션에서 이벤트 기반 아키텍처로 전환하기 5편 : Saga 패턴 운영 완성 - 분산 트레이싱, 데이터 이상현상 대응, Eventual Consistency 설계
ebson 2026. 3. 11. 22:074편에서 Saga의 실패 복구 전략을 다루었습니다. 보상 트랜잭션과 재시도, 멱등성, 다단계 실패 격리까지 설계하면 Saga의 에러핸들링 체계가 갖춰집니다. 그런데 이 체계가 실제 운영 환경에서 의도대로 동작하고 있는지를 어떻게 확인할 수 있을까요? 보상 트랜잭션이 정상적으로 실행되었는지, Dead Letter Topic에 메시지가 쌓이고 있지는 않은지, 어떤 단계에서 지연이 발생하는지를 파악하려면 체계적인 관찰가능성(Observability)이 필요합니다.
또한 여러 Saga가 동시에 실행되면서 같은 데이터에 접근할 때 발생할 수 있는 데이터 이상현상도 고려해야 합니다. 이 글에서는 Saga 시스템의 관찰가능성 설계, 동시 Saga 실행 시의 데이터 정합성 심화 주제, 그리고 시리즈 전체를 관통하는 설계 패러다임의 전환을 정리합니다.
Saga 관찰가능성이 중요한 이유
단일 서비스의 트랜잭션은 데이터베이스 로그와 애플리케이션 로그만으로도 상태를 파악할 수 있습니다. 하지만 Saga는 여러 서비스에 걸쳐 비동기적으로 진행되는 흐름이므로, 하나의 서비스 로그만으로는 전체 그림을 볼 수 없습니다.
Microsoft Azure Architecture Center의 Saga 패턴 문서에서는 이를 명확히 지적합니다.
"Monitoring and tracking the workflow of a saga are essential tasks to maintain operational oversight."
"Complexity of debugging sagas: Debugging sagas can be complex, specifically as the number of participating services grows."
Saga의 워크플로우를 모니터링하고 추적하는 것은 운영 가시성 확보에 필수적이며, 참여 서비스가 늘어날수록 디버깅 복잡도가 증가한다는 것입니다. Saga의 특정 단계에서 지연이 발생하거나 실패가 반복될 때, 이를 빠르게 감지하고 원인을 추적할 수 있는 체계가 없으면 운영 안정성을 확보하기 어렵습니다.
분산 트레이싱 - Saga 흐름 추적
Saga의 관찰가능성에서 가장 기본이 되는 것은 분산 트레이싱입니다. Saga의 각 단계가 서로 다른 서비스에서 비동기적으로 실행되므로, 하나의 Saga 인스턴스에 속한 작업들을 연결하여 추적할 수 있어야 합니다.
이를 위해 Correlation ID를 활용합니다. Saga가 시작될 때 고유한 Correlation ID를 생성하고, 이후 발행되는 모든 메시지에 이 ID를 포함시킵니다. 각 서비스는 메시지를 수신할 때 Correlation ID를 로그에 기록하고, 다음 서비스로 전달하는 메시지에도 동일한 ID를 포함시킵니다. 이렇게 하면 로그를 Correlation ID로 검색하여 하나의 Saga가 어떤 서비스를 거쳐 어떤 순서로 진행되었는지를 추적할 수 있습니다.
Saga의 단계별 상태 전이도 추적 대상입니다. 각 단계가 시작, 진행 중, 성공, 실패, 보상 중, 보상 완료 등 어떤 상태에 있는지를 기록하면, 특정 시점에 Saga가 어디까지 진행되었고 어디에서 멈췄는지를 파악할 수 있습니다.
Orchestration과 Choreography에서 분산 트레이싱의 구현 난이도에는 차이가 있습니다. Orchestration에서는 중앙의 Orchestrator가 전체 상태를 보유하므로, Orchestrator의 로그와 상태 저장소만으로도 Saga 흐름을 상당 부분 파악할 수 있습니다. Microsoft 문서에서도 Orchestration의 장점으로 흐름의 가시성을 꼽습니다.
Choreography에서는 중앙 제어 지점이 없으므로, 각 서비스의 로그를 Correlation ID로 연결하여 사후적으로 흐름을 재구성해야 합니다. 참여 서비스가 늘어날수록 이벤트 체인의 추적이 복잡해지며, 분산 트레이싱 인프라(Jaeger, Zipkin 등)의 도입이 사실상 필수적입니다.
메트릭과 알림 설계
분산 트레이싱이 개별 Saga의 흐름을 추적하는 데 초점을 맞춘다면, 메트릭은 시스템 전체의 건강 상태를 조감하는 데 사용됩니다.
Saga 시스템에서 추적해야 할 핵심 메트릭은 크게 세 가지입니다.
첫째, Saga 단위의 성공, 실패, 보상 트랜잭션 건수입니다. 실패율이 갑자기 증가하거나 보상 트랜잭션 비율이 높아지면, 특정 서비스나 외부 의존성에 문제가 발생했을 가능성이 높습니다.
둘째, 단계별 처리 지연시간(latency)입니다. 특정 단계의 지연이 증가하면 해당 서비스의 성능 저하나 리소스 부족을 의심할 수 있습니다.
셋째, Dead Letter Topic 적재 건수입니다. DLT에 메시지가 쌓이는 속도가 높아지면 정상적으로 처리되지 못하는 메시지가 증가하고 있다는 신호입니다.
이 메트릭들에 기반한 알림 전략도 함께 설계해야 합니다. DLT에 메시지가 적재되면 즉시 알림을 발송하여 운영팀이 원인을 분석할 수 있도록 합니다. 3편에서 다룬 Outbox 테이블의 미처리 건수가 임계치를 초과하면 Message Relay의 장애를 의심해야 합니다. 보상 트랜잭션의 실패도 알림 대상입니다. 4편에서 살펴본 것처럼 Microsoft 문서에서 "Compensating transactions might not always succeed"라고 경고하듯, 보상 실패는 시스템을 불일치 상태에 빠뜨릴 수 있으므로 즉각적인 대응이 필요합니다.
동시 Saga 실행과 데이터 이상현상
지금까지는 하나의 Saga가 단독으로 실행되는 상황을 전제했습니다. 하지만 실제 운영 환경에서는 여러 Saga가 동시에 실행되며, 같은 데이터에 접근할 수 있습니다. 이때 Saga 간의 격리가 보장되지 않으면 데이터 이상현상(Anomalies)이 발생할 수 있습니다.
실무 시나리오를 생각해 봅니다. 주문 Saga가 재고를 차감하는 동안, 재고 조정 Saga가 같은 재고 데이터를 변경한다면 어떻게 될까요? 또는 하나의 Saga가 보상 트랜잭션을 실행 중인 중간 상태에서, 다른 Saga가 이 데이터를 읽는다면요?
Saga는 각 서비스의 로컬 트랜잭션으로 구성되므로, 서비스 내부에서는 ACID가 보장되지만 서비스 간에는 격리(Isolation)가 제공되지 않습니다. Microsoft 문서에서는 이로 인해 발생할 수 있는 세 가지 데이터 이상현상을 제시합니다.
Lost Updates는 한 Saga의 변경을 다른 Saga가 덮어쓰는 현상입니다. 주문 Saga가 재고를 100에서 95로 차감한 직후, 재고 조정 Saga가 같은 재고를 100에서 110으로 변경하면, 주문 Saga의 차감이 사라집니다. Microsoft 문서에서는 이를 "When one saga modifies data without considering changes made by another saga, it results in overwritten or missing updates"라고 설명합니다.
Dirty Reads는 아직 완료되지 않은 Saga의 중간 상태를 다른 Saga가 읽는 현상입니다. 주문 Saga가 재고를 차감했지만 아직 결제 단계가 완료되지 않은 상태에서, 다른 Saga가 이 차감된 재고 값을 기준으로 의사결정을 한다면 문제가 됩니다. 이후 주문 Saga가 결제 실패로 보상 트랜잭션을 실행하여 재고를 복원하면, 다른 Saga는 잘못된 데이터를 기반으로 처리를 수행한 셈이 됩니다. Microsoft 문서에서는 "When a saga or transaction reads data that another saga has modified, but the modification isn't complete"라고 정의합니다.
Fuzzy Reads(Nonrepeatable Reads)는 Saga의 서로 다른 단계에서 같은 데이터를 읽었을 때, 그 사이에 다른 Saga의 변경으로 값이 달라지는 현상입니다. Microsoft 문서에서는 "When different steps in a saga read inconsistent data because updates occur between the reads"라고 설명합니다.
데이터 이상현상의 대응 전략
Microsoft 문서에서는 이러한 이상현상에 대응하기 위한 여섯 가지 전략을 제시합니다.
Semantic Lock은 Saga의 Compensable 단계가 실행 중임을 표시하는 애플리케이션 레벨의 잠금입니다. 예를 들어 주문의 상태를 "처리 중"으로 설정하여, 다른 Saga가 이 주문을 변경하지 못하도록 합니다. Saga가 완료되거나 보상이 완료되면 잠금을 해제합니다. 데이터베이스 레벨의 락이 아닌 비즈니스 상태를 활용한 논리적 잠금이라는 점이 특징입니다.
Commutative Updates는 순서에 관계없이 같은 결과를 내는 연산으로 업데이트를 설계하는 전략입니다. Microsoft 문서에서는 "Design updates so that they can be applied in any order while still producing the same result"라고 설명합니다. 예를 들어 "잔액에서 100을 차감"과 "잔액에 50을 추가"는 어떤 순서로 실행해도 최종 결과가 같습니다. 이 방식이 적용 가능한 경우 Saga 간의 실행 순서가 달라져도 최종 상태의 일관성을 유지할 수 있습니다.
Pessimistic View는 Saga 내의 단계 순서를 재배치하여 Dirty Read를 방지하는 전략입니다. Microsoft 문서에서는 "Reorder the sequence of the saga so that data updates occur in retryable transactions to eliminate dirty reads"라고 설명합니다. 데이터 업데이트를 Retryable 단계(Pivot 이후)에서 수행하도록 순서를 조정하면, Compensable 단계에서의 중간 상태 노출을 줄일 수 있습니다.
Reread Value는 데이터를 업데이트하기 전에 값이 변경되지 않았는지 다시 확인하는 전략입니다. Microsoft 문서에서는 "Confirm that data remains unchanged before you make updates. If data changes, stop the current step and restart the saga as needed"라고 설명합니다. Optimistic Locking과 유사한 접근으로, 값이 변경되었으면 현재 단계를 중단하고 Saga를 재시작합니다.
Version File은 레코드에 대한 모든 연산 이력을 유지하여 올바른 순서로 실행되도록 보장하는 전략입니다. Microsoft 문서에서는 "Maintain a log of all operations performed on a record and ensure that they're performed in the correct sequence to prevent conflicts"라고 설명합니다. 연산 이력을 기반으로 충돌을 감지하고 순서를 재조정할 수 있습니다.
Risk-based Concurrency based on Value는 비즈니스 위험도에 따라 동시성 제어 수준을 동적으로 결정하는 전략입니다. Microsoft 문서에서는 "Dynamically choose the appropriate concurrency mechanism based on the potential business risk"라고 설명합니다. 위험도가 낮은 업데이트에는 Saga의 Eventual Consistency를 허용하고, 위험도가 높은 업데이트에는 더 강한 동시성 제어(또는 분산 트랜잭션)를 적용하는 실용적 접근입니다.
이 여섯 가지 전략은 상호 배타적이지 않으며, 시스템의 요구사항과 비즈니스 특성에 따라 조합하여 사용합니다. 모든 데이터에 동일한 수준의 보호를 적용하는 것이 아니라, 데이터의 중요도와 동시 접근 가능성에 따라 적절한 전략을 선택하는 것이 실용적입니다.
Eventual Consistency를 수용하는 설계 마인드셋
이 시리즈를 통해 살펴본 모든 패턴과 전략의 근저에는 하나의 패러다임 전환이 있습니다. 강한 일관성(Strong Consistency)에서 최종 일관성(Eventual Consistency)으로의 전환입니다.
1편에서 살펴본 것처럼 MSA 환경에서 모든 참여자를 동기적으로 조율하여 강한 일관성을 보장하는 것은 가용성과 확장성을 심각하게 제약합니다. Saga 패턴은 이 제약을 우회하여, "일시적으로 불일치 상태가 존재할 수 있지만, 최종적으로는 일관된 상태에 도달한다"는 전제를 수용합니다.
이 전제를 수용하면 설계의 관점이 달라집니다. "트랜잭션이 즉시 완료된다"는 가정 대신, "트랜잭션이 최종적으로 완료된다"는 가정으로 시스템을 설계합니다. 중간 상태가 존재할 수 있으므로 이를 사용자에게 적절히 전달하는 UX가 필요하고, 중간 상태에서의 데이터 접근에 대한 보호 전략(이 글에서 다룬 이상현상 대응)이 필요하며, 최종 상태에 반드시 도달할 수 있도록 재시도와 복구 메커니즘(4편에서 다룬 다단계 복구)이 필요합니다.
이것은 단순히 기술적인 선택이 아니라, 분산 시스템에 대한 사고방식의 전환입니다. 모놀리스에서 당연했던 "즉시 일관성"을 포기하는 대신, 더 높은 가용성과 확장성, 서비스 독립성을 얻습니다. Saga 패턴과 그것을 둘러싼 Outbox, 멱등성, 보상 트랜잭션, 관찰가능성은 모두 이 패러다임 전환을 안전하게 수행하기 위한 도구입니다.
시리즈 회고
5편에 걸쳐 "분산 트랜잭션에서 이벤트 기반 아키텍처로"의 여정을 정리했습니다. 각 편이 다룬 핵심을 되짚어 봅니다.
1편에서는 MSA 환경에서 분산 트랜잭션이 왜 깨지는지를 살펴보았습니다. @Transactional의 단일 리소스 한계,
ChainedTransactionManager의 부분 커밋 문제, 2PC의 가용성과 확장성 제약을 통해, 동기식 해법으로는 MSA의 데이터 정합성 문제를 근본적으로 해결할 수 없음을 확인했습니다.
2편에서는 이에 대한 대안으로 Saga 패턴의 구조를 다루었습니다. 로컬 트랜잭션의 연쇄와 보상 트랜잭션으로 분산 정합성을 보장하는 메커니즘, Orchestration과 Choreography의 특성과 선택 기준, 그리고 비동기 메시징이 Saga의 표준적인 통신 방식이 되는 이유를 정리했습니다.
3편에서는 비동기 메시징 도입 시 필연적으로 마주치는 이중 쓰기 문제와 Outbox 패턴을 다루었습니다. 비즈니스 데이터와 메시지를 동일한 로컬 트랜잭션으로 처리하는 구조, Polling Publisher와 Transaction Log Tailing이라는 두 가지 Message Relay 전략, 그리고 메시지 순서 보장과 at-least-once 발행 특성을 살펴보았습니다.
4편에서는 Saga의 에러핸들링을 다루었습니다. Backward Recovery와 Forward Recovery의 복구 방향, Compensable/Pivot/Retryable 트랜잭션 분류에 기반한 복구 전략, 멱등성의 구현 방법, 그리고 Consumer 재시도부터 Dead Letter Queue와 Outbox 배치까지의 다단계 실패 격리 전략을 정리했습니다.
5편에서는 운영 관점의 관찰가능성과 동시 Saga 실행 시의 데이터 이상현상 대응을 다루었습니다. 분산 트레이싱, 메트릭, 알림 전략으로 Saga의 운영 가시성을 확보하고, Lost Updates, Dirty Reads, Fuzzy Reads에 대한 대응 전략을 살펴보았습니다.
이 시리즈가 다룬 것은 결국 하나의 흐름입니다. 임시 해법(ChainedTransactionManager)으로는 해결할 수 없었던 분산 트랜잭션 문제를, Saga 패턴이라는 근본적인 대안으로 접근하고, 이를 안전하게 구현하기 위한 Outbox, 멱등성, 에러핸들링, 관찰가능성이라는 구성 요소를 하나씩 쌓아가는 과정입니다. 각 구성 요소는 독립적이지 않고 서로 맞물려 동작하며, 이 전체가 Eventual Consistency라는 패러다임 위에서 비로소 의미를 갖습니다.
분산 시스템의 데이터 정합성은 하나의 패턴이나 도구로 해결되는 문제가 아닙니다. 문제를 정확히 인식하고, 각 해법의 한계를 이해하며, 트레이드오프를 수용하면서 점진적으로 시스템을 진화시켜 나가는 과정 자체가 답에 가깝다고 생각합니다.
References
- Chris Richardson, Microservices Patterns (Manning, 2018) — Chapter 4: Managing transactions with sagas
- 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
'STUDY' 카테고리의 다른 글
- Total
- Today
- Yesterday
- Eager Initialization
- Hot Key 문제
- 캐시 성능 비교
- 트래픽 처리
- Spring Batch
- InterruptedException
- 백엔드 아키텍처
- Enum 기반 싱글톤
- mybatis
- 캐시와 인덱스
- Initialization-on-Demand Holder Idiom
- DB 인덱스 성능
- 캐시 장애
- spring batch 5
- Cache Aside
- Cache Penetration
- Redis 캐시 전략
- Double-Checked Locking
- TTL 설계
- Java Performance
- Redis 성능 개선
- Redis vs DB
- 트랜잭션 관리
- 백엔드 성능 튜닝
- DB 트랜잭션
- 스레드 생명주기
- 동시성처리
- 백엔드 성능 설계
- Cache Avalanche
- 백엔드 성능
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |

