티스토리 뷰
분산 트랜잭션에서 이벤트 기반 아키텍처로 전환하기 4편 : Saga 패턴 에러핸들링 - 보상 트랜잭션, 재시도, Dead Letter Queue 실전 설계
ebson 2026. 3. 11. 22:053편에서 Outbox 패턴으로 DB 변경과 메시지 발행의 원자성을 보장하는 방법을 살펴보았습니다. Outbox 패턴은 메시지가 "반드시 발행된다"는 보장을 제공하지만, 발행된 메시지를 수신한 서비스가 처리에 실패하면 어떻게 되는지에 대해서는 답하지 않습니다. Saga는 여러 서비스에 걸쳐 단계적으로 진행되는 흐름이므로, 어느 단계에서든 실패가 발생할 수 있고, 이에 대한 체계적인 복구 전략이 필요합니다.
이 글에서는 Saga 실패 시 보상 트랜잭션으로 되돌리거나 재시도로 전진하는 두 가지 복구 방향을 정리하고, 재시도의 전제 조건인 멱등성, 그리고 실패 메시지를 격리하는 구체적인 전략을 다룹니다.
복구의 두 가지 방향
Saga의 특정 단계에서 실패가 발생했을 때, 복구 방향은 크게 두 가지입니다. 이전 단계를 되돌리는 Backward Recovery와, 실패한 단계를 재시도하여 앞으로 나아가는 Forward Recovery입니다.
AWS Prescriptive Guidance의 Saga 패턴 문서에서는 이 두 가지를 다음과 같이 설명합니다.
Backward Recovery는 보상 원칙(compensation principle)에 따라 동작합니다. 실패 이전에 완료된 로컬 트랜잭션들의 변경을 되돌리는 보상 트랜잭션(compensating transaction)을 역순으로 실행하여 이전 상태를 복원합니다. AWS 문서에서는 애플리케이션 레벨의 실패, 예를 들어 결제 서비스에서 유효하지 않은 결제 정보로 인한 실패가 이 경우에 해당한다고 설명합니다.
"the saga pattern can perform a backward recovery by issuing a compensatory transaction to update the inventory and the order databases, and reinstate their previous state."
Forward Recovery는 연속 원칙(continuation principle)에 따라 동작합니다. 실패한 로컬 트랜잭션을 재시도하여 비즈니스 프로세스를 계속 진행하는 방식입니다. AWS 문서에서는 인프라 레벨의 일시적 장애가 이 경우에 해당한다고 설명합니다.
"the saga pattern can perform a forward recovery by retrying the local transaction and continuing the business process."
네트워크 타임아웃이나 일시적인 서비스 불가 같은 인프라 문제는 재시도로 해결될 가능성이 높지만, 비즈니스 규칙 위반이나 유효성 검증 실패는 재시도해도 같은 결과가 반복됩니다. 어떤 복구 방향을 선택할지는 실패의 성격에 따라 달라지며, 이를 체계적으로 판단하기 위해 Saga의 각 단계를 유형별로 분류하는 것이 도움이 됩니다.
Compensable, Pivot, Retryable - 단계의 분류
2편에서 간략히 소개한 Microsoft Azure Architecture Center의 트랜잭션 유형 분류를 복구 전략의 맥락에서 다시 살펴봅니다.
Compensable Transaction은 보상 트랜잭션으로 되돌릴 수 있는 단계입니다. Microsoft 문서에서는 "can be undone or compensated for by other transactions with the opposite effect"라고 설명합니다. 재고 차감, 임시 결제 승인 같은 단계가 여기에 해당합니다. 이 단계에서 실패가 발생하면, 이전에 완료된 Compensable 단계들을 역순으로 보상하는 Backward Recovery를 수행합니다.
Pivot Transaction은 Saga의 결정적 분기점입니다. Microsoft 문서에서는 이를 "the boundary between reversible and committed"라고 설명합니다. Pivot 이전에는 보상을 통한 되돌림이 가능하지만, Pivot이 성공한 이후에는 더 이상 되돌아갈 수 없습니다. 최종 결제 확정이나 외부 시스템에 대한 확정 요청처럼, 한번 실행되면 취소할 수 없는 성격의 단계가 Pivot에 해당합니다.
Retryable Transaction은 Pivot 이후에 위치하는 단계로, 반드시 완료되어야 하는 작업입니다. Microsoft 문서의 표현을 빌리면 "idempotent and help ensure that the saga can reach its final state, even if temporary failures occur"합니다. Pivot이 이미 성공했으므로 보상으로 되돌릴 수 없고, 재시도를 통해 최종 상태에 도달해야 합니다. 이 단계는 Forward Recovery의 대상이며, 멱등성이 보장되어야 합니다.
이 분류가 복구 전략 설계에 주는 시사점은 명확합니다. Pivot 이전 단계에서의 실패는 Backward Recovery로, Pivot 이후 단계에서의 실패는 Forward Recovery로 대응합니다. Saga를 설계할 때 각 단계가 어떤 유형에 해당하는지를 먼저 정의하면, 실패 시나리오별 복구 흐름이 자연스럽게 도출됩니다.
보상 트랜잭션 설계 시 유의점
Backward Recovery의 핵심인 보상 트랜잭션은 "실패 이전 단계를 역순으로 되돌리는 것"이라고 단순하게 설명할 수 있지만, 실제 설계에서는 몇 가지 유의점이 있습니다.
가장 중요한 점은 보상 트랜잭션이 항상 성공하는 것은 아니라는 것입니다. Microsoft 문서에서는 이를 명시적으로 경고합니다.
"Compensating transactions might not always succeed, which can leave the system in an inconsistent state."
보상 트랜잭션 자체가 실패하면 시스템이 불일치 상태에 빠질 수 있습니다. 예를 들어 재고 복원 보상 트랜잭션이 네트워크 장애로 실패하면, 재고는 차감된 채로 남아있지만 주문은 취소된 상태가 됩니다. 따라서 보상 트랜잭션도 재시도 가능하도록 설계해야 하며, 멱등성이 보장되어야 합니다. 같은 보상 트랜잭션이 여러 번 실행되더라도 결과가 동일해야 하는 것입니다.
또한 보상 트랜잭션은 단순한 "되돌리기(undo)"가 아닌 "비즈니스 의미의 역방향 작업"이라는 점도 중요합니다. 데이터베이스의 롤백과는 달리, 보상 트랜잭션은 이미 커밋된 변경에 대해 비즈니스 로직으로 구현된 역방향 작업을 수행합니다. 결제 승인에 대한 보상은 결제 취소 API 호출이고, 재고 차감에 대한 보상은 재고 복원 처리입니다. 각 보상 작업의 비즈니스 의미와 부수 효과를 정확히 이해하고 설계해야 합니다.
Orchestration과 Choreography에서의 복구 전략 차이
복구 전략의 구현 방식은 Saga의 조율 방식에 따라 달라집니다. Orchestration에서는 중앙의 Orchestrator가 Saga의 전체 상태를 관리하므로, 실패 시 복구 흐름도 Orchestrator가 통합적으로 제어합니다. 어떤 단계에서 실패했는지, 어떤 보상 트랜잭션을 어떤 순서로 실행해야 하는지를 Orchestrator가 판단하고 지시합니다. 재시도 정책, 타임아웃, 최대 재시도 횟수 같은 복구 관련 설정도 Orchestrator 한 곳에서 관리할 수 있습니다.
Choreography에서는 중앙 조율자가 없으므로, 각 서비스가 독립적으로 복구 전략을 관리해야 합니다. AWS Prescriptive Guidance의 Saga Choreography 문서에서는 이를 다음과 같이 언급합니다.
"In saga choreography, it's more difficult to implement timeouts, retries, and other resiliency patterns globally, compared with saga orchestration. Choreography must be implemented on individual components instead of at an orchestrator level."
타임아웃, 재시도 같은 복원력 패턴을 개별 서비스에 분산 구현해야 하므로, Orchestration에 비해 전역적인 복구 정책을 일관되게 적용하기가 어렵습니다. 서비스가 추가될수록 각 서비스의 복구 로직을 독립적으로 관리해야 하는 부담도 커집니다. AWS 문서에서도 "compensatory transactions and retries add complexities to the application code, which can result in maintenance overhead"라고 지적합니다.
이러한 차이는 Saga 조율 방식 선택 시 복구 전략의 복잡도를 함께 고려해야 함을 보여줍니다. 복구 흐름이 복잡한 경우에는 Orchestration의 중앙 집중식 관리가 유리하고, 단순한 흐름에서는 Choreography의 각 서비스가 자체적으로 복구를 처리하는 것이 충분할 수 있습니다.
멱등성 - 재시도의 전제 조건
Forward Recovery든 보상 트랜잭션의 재시도든, 재시도가 안전하게 동작하려면 멱등성(Idempotency)이 보장되어야 합니다. 멱등성이란 같은 작업을 여러 번 실행해도 결과가 동일한 성질을 말합니다.
Microsoft Azure Architecture Center에서는 이를 다음과 같이 설명합니다.
"ensure idempotence, when repeating the same operation doesn't alter the outcome"
AWS Prescriptive Guidance에서도 같은 맥락으로 강조합니다.
"Saga participants have to be idempotent to allow repeated execution in case of transient failures that are caused by unexpected crashes and orchestrator failures."
일시적 장애나 Orchestrator 장애로 인해 같은 요청이 반복 실행될 수 있으므로, Saga 참여 서비스들은 반드시 멱등성을 갖춰야 한다는 것입니다.
멱등성을 구현하는 방법은 크게 세 가지로 나눌 수 있습니다.
첫째, Idempotency Key 기반 중복 요청 방지입니다. 각 메시지에 고유한 식별자를 부여하고, 수신 서비스가 이미 처리한 메시지 ID를 추적하여 중복 처리를 방지합니다. 3편에서 살펴본 것처럼 Outbox 패턴의 Message Relay는 구조적으로 최소 1회 발행(at-least-once) 특성을 가지므로, 중복 메시지 수신에 대한 방어가 필수적입니다. Microservices.io에서도 "consumers must be idempotent, often by tracking processed message IDs"라고 설명합니다.
둘째, 상태 기반 판단입니다. 요청을 처리하기 전에 현재 상태를 확인하여, 이미 처리 완료된 상태라면 실제 작업을 수행하지 않고 성공 응답을 반환합니다. 예를 들어 결제 서비스가 "결제 완료" 상태의 주문에 대해 결제 요청을 다시 받으면, 중복 결제를 실행하지 않고 기존 결제 결과를 반환합니다.
셋째, 연산 자체를 멱등하게 설계하는 것입니다. "재고를 5개 차감"이 아닌 "재고를 95개로 설정"처럼, 절대값 기반의 연산은 여러 번 실행해도 결과가 동일합니다. 단, 이 방식은 동시성 제어와 함께 사용해야 안전합니다.
실무에서는 이 세 가지를 조합하여 사용하는 것이 일반적입니다. Idempotency Key로 명시적인 중복을 차단하고, 상태 기반 검증으로 논리적 중복을 방어하며, 가능한 경우 연산 자체를 멱등하게 설계하여 다중 방어선을 구축합니다.
실패 메시지의 다단계 격리
멱등성이 보장되더라도 모든 재시도가 성공하는 것은 아닙니다. 네트워크 장애가 지속되거나, 의존 서비스가 장시간 복구되지 않거나, 메시지 자체에 문제가 있을 수 있습니다. 이러한 상황에서 실패한 메시지를 어떻게 격리하고 복구할 것인지에 대한 전략이 필요합니다.
Consumer 재시도
가장 먼저 시도하는 것은 Consumer 레벨의 즉각적인 재시도입니다. 메시지 처리가 실패하면 설정된 횟수만큼 재시도를 수행합니다. 일시적인 네트워크 지연이나 타임아웃처럼 짧은 시간 내에 복구되는 장애는 이 단계에서 해결됩니다. 재시도 시에는 지수 백오프(Exponential Backoff) 전략을 적용하는 것이 일반적입니다. 첫 번째 재시도는 1초 후, 두 번째는 2초 후, 세 번째는 4초 후처럼 간격을 점진적으로 늘려 장애 상태의 서비스에 과도한 부하를 가하지 않도록 합니다.
지연 토픽을 통한 점진적 재시도
Consumer 재시도 횟수를 초과한 메시지는 지연 토픽(Delay Topic)으로 라우팅합니다. 지연 토픽은 메시지를 일정 시간 동안 대기시킨 후 다시 원래 토픽으로 전달하는 구조입니다. 단계별로 TTL을 증가시켜 30초, 1분, 5분처럼 점진적으로 재시도 간격을 늘립니다.
이 전략은 Consumer 재시도만으로는 해결되지 않지만, 시간이 지나면 복구될 가능성이 있는 장애에 효과적입니다. 의존 서비스가 재시작 중이거나 일시적인 리소스 부족 상태일 때, 충분한 시간을 두고 재시도함으로써 자동 복구의 기회를 확보합니다.
Dead Letter Queue를 통한 격리
지연 토픽의 재시도까지 모두 실패한 메시지는 Dead Letter Topic(DLT)으로 이동합니다. DLT는 정상적인 처리 흐름에서 벗어난 메시지를 격리하여 보관하는 역할을 합니다. DLT에 적재된 메시지에는 원본 토픽, 파티션, 오프셋, 실패 사유 등의 메타데이터를 헤더에 기록하여, 이후 원인 분석과 재처리에 필요한 정보를 보존합니다.
DLT의 핵심 가치는 실패한 메시지가 정상 메시지의 처리를 방해하지 않도록 격리하는 것입니다. DLT 없이 실패한 메시지를 계속 재시도하면, 해당 파티션의 오프셋이 진행되지 않아 뒤따르는 정상 메시지의 처리도 차단됩니다. DLT로 격리함으로써 정상 흐름의 중단 없이 실패 건을 별도로 관리할 수 있습니다.
DLT에 적재된 메시지는 운영팀이 원인을 분석한 후 수동으로 재처리하거나, 원인이 해소된 것이 확인되면 원래 토픽으로 재발행하는 방식으로 처리합니다.
Outbox 기반 배치 보완
Message Relay 자체의 발행 실패나, Consumer가 메시지를 수신조차 하지 못하는 상황에 대비한 보완 장치도 필요합니다. 3편에서 다룬 Outbox 테이블에는 각 메시지의 발행 상태가 기록되어 있으므로, CronJob 배치가 주기적으로 Outbox의 미처리 건을 스캔하여 재발행을 시도할 수 있습니다.
이 배치는 Message Relay의 장애나 컨테이너 완전 장애 같은 극단적 상황에서의 안전망 역할을 합니다. 정상 상황에서는 Message Relay가 실시간으로 발행을 처리하고, 배치는 누락된 건이 있는지를 확인하는 보완적 역할만 수행합니다.
다단계 복구의 종합
지금까지의 전략을 종합하면 다음과 같은 다단계 복구 흐름이 구성됩니다.
1차로 Consumer가 즉각 재시도를 수행합니다. 일시적 장애는 이 단계에서 해소됩니다. 2차로 Consumer 재시도가 초과되면 지연 토픽으로 이동하여 점진적 간격의 재시도가 이루어집니다. 3차로 지연 토픽의 재시도까지 실패하면 DLT로 격리하여 정상 흐름을 보호하고, 원인 분석 후 재처리합니다. 4차로 CronJob 배치가 Outbox의 미처리 건을 주기적으로 스캔하여, Message Relay 장애 등으로 인한 발행 누락을 보완합니다.
각 단계는 이전 단계에서 해결되지 않은 실패를 다음 단계로 넘기는 구조입니다. 모든 실패를 한 곳에서 처리하려 하지 않고, 실패의 심각도에 따라 단계별로 대응함으로써 정상 흐름의 영향을 최소화하면서 복구 가능성을 극대화합니다.
정리
Saga의 에러핸들링은 두 가지 복구 방향에서 출발합니다. Pivot 이전 단계의 비즈니스 실패에는 보상 트랜잭션으로 되돌리는 Backward Recovery를, Pivot 이후 단계의 일시적 실패에는 재시도로 전진하는 Forward Recovery를 적용합니다. 재시도가 안전하게 동작하려면 멱등성이 전제되어야 합니다. Idempotency Key, 상태 기반 판단, 멱등한 연산 설계를 조합하여 중복 실행에 대한 방어를 구축합니다.
실패한 메시지는 Consumer 재시도, 지연 토픽, Dead Letter Queue, Outbox 기반 배치라는 다단계 격리 전략으로 관리합니다. 각 단계가 이전 단계에서 해결되지 않은 실패를 넘겨받아 처리하므로, 정상 흐름을 방해하지 않으면서 복구 기회를 확보할 수 있습니다.
다음 편에서는 이렇게 구축된 Saga 시스템의 운영 안정성을 확보하기 위한 모니터링과 관찰가능성, 그리고 동시에 실행되는 Saga 간의 데이터 정합성 심화 주제를 다루겠습니다.
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
- 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
- Microservices.io — Transactional Outbox: https://microservices.io/patterns/data/transactional-outbox.html
'STUDY' 카테고리의 다른 글
- Total
- Today
- Yesterday
- Eager Initialization
- 백엔드 성능 튜닝
- 캐시와 인덱스
- 트랜잭션 관리
- 동시성처리
- 백엔드 성능 설계
- Double-Checked Locking
- Redis 캐시 전략
- Cache Aside
- TTL 설계
- Java Performance
- Cache Penetration
- Hot Key 문제
- spring batch 5
- DB 트랜잭션
- InterruptedException
- 캐시 성능 비교
- mybatis
- 캐시 장애
- Cache Avalanche
- Redis vs DB
- 백엔드 아키텍처
- 백엔드 성능
- 스레드 생명주기
- Enum 기반 싱글톤
- Initialization-on-Demand Holder Idiom
- 트래픽 처리
- Spring Batch
- DB 인덱스 성능
- Redis 성능 개선
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |

