이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › 아키텍처와 운영 › 서비스 분리
Saga와 Outbox
서비스마다 DB가 따로일 때 "전부 성공 아니면 전부 취소"를 어떻게 하나
주문, 결제, 재고, 배송이 각자의 DB를 가진 서비스로 나뉘어 있으면 하나의 DB 트랜잭션으로 묶을 수 없습니다. 중간에 실패하면 이미 끝난 단계를 되돌리는 작업을 따로 실행해야 합니다(Saga). 또 DB 저장과 메시지 발행 사이에 서버가 죽으면 둘이 어긋납니다(Outbox가 해결하는 문제).
문서·표준 대조 · microservices.io의 패턴 설명
이 실습에 나오는 개념
- 분산 트랜잭션과 Saga 패턴
- 여러 서비스(또는 데이터베이스)에 걸친 작업을 하나의 트랜잭션처럼 처리하는 문제다. 서비스마다 데이터베이스가 따로 있으면 하나의 로컬 트랜잭션으로 묶을 수 없다. 여러 데이터베이스를 묶는 2단계 커밋(2PC)이 있지만 가용성과 결합도 부담이 커 마이크로서비스에서는 잘 쓰지 않는다. Saga 패턴은 각 서비스가 자신의 작업을 차례로 커밋하고, 중간에 실패하면 이미 완료된 작업을 취소하는 보상 작업을 실행해 일관성을 맞추는 방법이다.
- 보상 트랜잭션
- 이미 커밋된 작업의 효과를 되돌리기 위해 실행하는 별도의 작업이다. 데이터베이스의 롤백과 달리 반대 동작(예: 결제 취소, 재고 복원)을 수행하는 새로운 트랜잭션이다. Saga 패턴에서 실패 시 이전 단계들을 취소하는 데 쓴다.
- Transactional Outbox 패턴
- 데이터베이스 변경과 메시지 발행을 함께 성공시키기 위한 패턴이다. 메시지를 직접 발행하는 대신, 업무 데이터를 변경하는 것과 같은 트랜잭션에서 보낼 메시지를 별도의 테이블(outbox)에 저장한다. 이후 다른 프로세스가 이 테이블을 읽어 메시지 브로커로 발행한다. DB에는 반영되었는데 메시지가 발행되지 않는 불일치를 막는다.
- 메시지 전달 보장
- 메시지가 몇 번 전달되는지에 대한 보장 수준이다. at-most-once는 최대 한 번 전달되어 중복은 없지만 유실될 수 있다. at-least-once는 최소 한 번 전달되어 유실은 없지만 중복될 수 있다. exactly-once는 정확히 한 번 전달된다. 실무에서는 at-least-once로 두고 소비자가 중복을 처리(멱등)하도록 만드는 경우가 많다.
- 최종 일관성(Eventual Consistency)
- 데이터를 변경한 직후에는 일부 노드가 이전 값을 보여 줄 수 있지만, 추가 변경이 없으면 시간이 지나 모든 노드가 같은 값을 갖게 된다는 보장이다. 즉시 일관성을 포기하는 대신 가용성과 확장성을 얻는다. 분산 데이터베이스와 마이크로서비스 간 데이터 동기화에서 흔히 채택된다.
이렇게 바꿔 보세요
- 처음 설정으로 — 재고 차감에서 실패. 결제와 주문을 거꾸로 되돌립니다.
- 마지막 단계에서 실패 — 배송 요청 실패. 되돌릴 것이 세 개입니다.
- 실패 없음 — 네 단계가 차례로 성공합니다.
- 커밋 후 발행하다 서버가 죽으면 — 주문은 저장됐는데 이벤트가 나가지 않습니다.
- 발행 후 커밋하다 서버가 죽으면 — 이벤트는 나갔는데 주문이 없습니다.
- Outbox 테이블 사용 — 주문과 "보낼 메시지"를 한 트랜잭션에 저장합니다.
처음 설정으로 실행하면
- 사용자가 주문합니다. 네 서비스가 각자 자기 DB에 작업을 커밋하고, 끝나면 다음 서비스에 이벤트로 알립니다. 전체를 감싸는 하나의 트랜잭션은 없습니다.
- 1단계 주문 서비스: 주문 생성 완료. 자기 DB에 커밋하고 다음 단계를 위한 이벤트를 발행합니다.
- 2단계 결제 서비스: 결제 승인 완료. 자기 DB에 커밋하고 다음 단계를 위한 이벤트를 발행합니다.
- 3단계 재고 서비스: 재고 차감에 실패했습니다. 이 서비스의 로컬 트랜잭션은 롤백되지만, 앞 단계들은 이미 각자의 DB에 커밋된 상태입니다. DB 롤백으로는 되돌릴 수 없습니다.
- 보상 1: 결제 서비스가 실패 이벤트를 받아 결제 취소(환불)를 실행합니다. 이미 커밋된 작업을 "취소하는 새 작업"으로 되돌리는 것입니다. 실행한 순서의 반대로 진행합니다.
- 보상 2: 주문 서비스가 실패 이벤트를 받아 주문 취소를 실행합니다. 이미 커밋된 작업을 "취소하는 새 작업"으로 되돌리는 것입니다. 실행한 순서의 반대로 진행합니다.
- 모든 보상이 끝났습니다. 주문은 취소 상태가 됐고 데이터는 다시 앞뒤가 맞습니다. 다만 보상이 끝나기 전까지는 "결제는 됐는데 주문은 실패"처럼 어긋난 상태가 잠시 존재했습니다.
실행 결과
- Saga는 긴 트랜잭션을 서비스별 로컬 트랜잭션의 연속으로 나누고, 실패하면 보상 트랜잭션으로 되돌립니다. DB의 롤백처럼 "없던 일"이 되는 것이 아니라 취소 기록이 남는 새 작업입니다. 그래서 중간 상태가 외부에 보일 수 있고(격리되지 않음), 보상 작업 자체도 실패할 수 있어 재시도와 멱등성이 필요합니다.
코드
// 각 단계는 자기 서비스의 로컬 트랜잭션으로 커밋된다
1. 주문 서비스 주문 생성 (PENDING) 보상: 주문 취소
2. 결제 서비스 결제 승인 보상: 결제 취소(환불)
3. 재고 서비스 재고 차감 보상: 재고 복구
4. 배송 서비스 배송 요청 (마지막 단계)
// 단계가 실패하면 "실패" 이벤트를 발행하고,
// 앞선 단계들이 그 이벤트를 받아 자기 보상 작업을 실행한다
이 모형의 전제
Saga는 서비스들이 이벤트를 주고받으며 진행하는 방식(코레오그래피)으로 그렸습니다. 중앙의 조정자가 각 단계를 지시하는 방식(오케스트레이션)도 있으며 보상의 원리는 같습니다. 보상 작업은 항상 성공한다고 단순화했습니다. Outbox 예에서는 서버가 죽는 시점을 가장 문제가 되는 순간으로 고정했습니다. 패턴의 정의는 마이크로서비스 패턴 문헌(microservices.io)의 설명을 따랐습니다.
앞 실습: 서킷 브레이커 · 다음 실습: 게이트웨이와 점진적 전환