Transactional Outbox와 Saga: 원자성·중복 처리·보상의 차이

anlee·2025년 5월 3일

매일매일 블로그

목록 보기
4/49
post-thumbnail

Transactional Outbox와 Saga는 함께 언급되지만 해결하는 문제의 범위가 다르다. Outbox는 DB 변경과 발행할 이벤트의 기록을 묶고, Saga는 여러 서비스에 걸친 업무의 진행과 보상을 조정한다. 둘 중 하나만 선택해야 하는 관계는 아니다.

1. Outbox가 원자적으로 묶는 범위

주문을 DB에 저장한 뒤 OrderCreated를 브로커로 전송한다고 하자. DB 커밋 후 발행 전에 서버가 종료되면 주문만 남고 이벤트는 발행되지 않는다. 반대로 이벤트부터 발행하면 주문 저장이 실패했는데 다른 서비스는 주문 생성 이벤트를 받을 수 있다.

Outbox는 업무 테이블과 outbox 테이블을 같은 DB 트랜잭션에서 저장해 이 간격을 줄인다.

주문 서비스의 DB 트랜잭션:
  주문 저장
  outbox에 eventId·주문 ID·eventType·payload 저장
  COMMIT

별도 relay:
  미발행 outbox 조회 또는 CDC로 변경 감지
  브로커에 발행하고 수신 확인
  발행 상태 기록

Outbox 테이블과 이벤트 전달 구성

그림은 SNS·SQS를 사용하는 구성 예다. 업무 데이터와 outbox 레코드의 저장은 원자적이지만, DB 커밋과 브로커 수신이 하나의 트랜잭션이 되는 것은 아니다. polling과 CDC 중 어떤 전달 방식을 사용해도 장애·재시도 경계를 살펴봐야 한다.

2. 왜 중복 발행이 생길 수 있을까?

relay가 이벤트를 발행하고 브로커가 받았지만, outbox를 SENT로 바꾸기 전에 프로세스가 종료될 수 있다. 재시작한 relay는 같은 이벤트를 다시 발행한다. 반대로 발행 전에 SENT로 기록하면 그 뒤 발행 실패 시 이벤트가 누락될 수 있다.

따라서 Outbox를 “정확히 한 번 발행” 또는 “이벤트 유실 0%”라는 포괄적 보장으로 설명하면 안 된다. 보통 재전달을 허용하고 소비자가 중복을 처리하는 설계가 필요하다. DB·브로커의 내구성 설정, 복구, 보관 기간과 운영 상태도 전달에 영향을 준다. AWS Transactional Outbox 안내

소비자에서는 이벤트 ID로 처리 이력을 구분할 수 있다. 다음은 소비자 DB에 업무 변경과 inbox를 같이 저장하는 의사 코드다.

소비자 DB 트랜잭션 시작
  consumerName + eventId를 inbox에 삽입 시도
  이미 처리된 ID라면 업무 변경 없이 종료
  처음 받은 ID라면 업무 상태 변경
  COMMIT
그 뒤 브로커 ACK 또는 offset 처리

inbox에는 유니크 제약을 둔다. 삽입과 업무 변경이 같은 트랜잭션이므로 실패하면 둘 다 롤백된다. 커밋 후 ACK 전에 죽어 재전달되더라도 저장된 이력으로 중복 적용을 막는다. inbox 보관 기간은 메시지 재전달·재처리 가능한 기간과 맞춰야 한다.

이 방법도 DB 밖의 이메일 발송이나 결제까지 자동으로 원자화하지 않는다. 외부 호출은 별도의 멱등 키나 다시 outbox로 넘기는 방식이 필요하다. Kafka 트랜잭션을 사용해도 외부 DB·HTTP 부작용까지 모두 같은 보장에 포함되는 것은 아니다.

3. Saga는 업무 단계와 실패 이후를 정의한다

예를 들어 주문, 재고, 결제가 서로 다른 서비스와 DB에 있다고 하자.

진행 단계성공 시 다음 단계이후 실패 시 고려할 보상
주문 접수재고 예약 요청주문 취소 상태 전환
재고 예약결제 요청예약한 재고 해제
결제 승인주문 확정결제 취소 또는 환불

보상은 이전 DB 트랜잭션을 물리적으로 되돌리는 rollback과 다르다. 이미 보낸 이메일을 되돌릴 수는 없고, 환불에는 별도의 기록과 실패 가능성이 있다. 취소가 불가능한 시점이나 수동 확인이 필요한 상태도 업무 규칙에 포함해야 한다.

여러 서비스의 Saga 진행과 보상 개념

Saga의 중간 상태는 다른 요청에서 관찰될 수 있다. 예를 들어 예약 중인 재고를 다른 주문이 구매할 수 있는지, 취소 중인 주문에 배송을 시작해도 되는지 정해야 한다. Saga라는 이름만으로 전역 격리나 자동 보상이 생기지는 않는다.

4. Choreography와 Orchestration 선택

방식장점설계할 부분
Choreography각 서비스가 이벤트를 받아 다음 동작을 수행이벤트 의존관계, 순환, 전체 진행 상태 추적
Orchestration조정자가 명령과 결과를 관리해 흐름을 보기 쉬움진행 상태의 내구성, 조정자 복구와 중복 명령 처리

서비스 개수가 특정 숫자를 넘으면 무조건 한 방식이 정답이라는 기준은 없다. 단계 수보다 분기·시간 제한·보상·운영 추적의 복잡성을 본다. Orchestrator도 상태 저장과 다중 인스턴스 복구를 설계할 수 있으므로, 본질적으로 단일 장애점이라고 단정하지 않는다.

5. 두 패턴을 함께 사용하는 방법

주문 서비스가 상태를 PENDING으로 저장하면서 재고 예약 명령을 outbox에 기록할 수 있다. 재고 서비스는 inbox로 중복 명령을 구분하고, 재고 예약과 결과 이벤트를 자신의 DB 트랜잭션에 저장한다. Saga는 그 결과를 받아 다음 단계 또는 보상을 결정한다.

이때 필요한 식별자는 서로 역할이 다르다. sagaId는 전체 업무를, eventId는 개별 메시지를, 주문 ID 같은 aggregate ID는 변경 대상과 순서 기준을 나타낸다. 재시도할 때 매번 다른 이벤트 ID를 생성하면 소비자의 중복 판별이 무력해질 수 있다.

여러 relay가 동시에 실행될 때는 작업 선점·lease 만료·재처리를 설계한다. 순서가 중요한 이벤트는 같은 aggregate의 순번과 브로커 파티션·라우팅을 맞춘다. Outbox 테이블에 저장된 시각만으로 서비스 전체의 전역 순서가 보장되지는 않는다.

운영에서는 미발행 건수뿐 아니라 가장 오래된 미발행 시간, 반복 실패, 중복 처리, 보상 대기 시간을 본다. 메시지가 계속 쌓이는데 API 응답만 성공한다고 시스템이 정상인 것은 아니다. Outbox는 전달할 사실을 남기고, Saga는 그 사실을 바탕으로 업무를 끝내는 책임을 맡는다.

0개의 댓글