트랜잭셔널 아웃박스
개요
트랜잭셔널 아웃박스(transactional outbox)는 업무 데이터의 변경과 나중에 전달할 메시지를 같은 데이터베이스 트랜잭션에 기록하는 패턴이다. 전달자는 커밋된 메시지를 별도로 읽어 브로커나 수신 서비스에 보낸다.
이 구조가 필요한 이유는 한 업무의 결과를 두 시스템에 남길 때 실패 지점이 생기기 때문이다. 주문 DB를 커밋한 뒤 메시지를 보내기 전에 프로세스가 종료되면, 주문은 있지만 다른 서비스는 주문이 생겼다는 사실을 모른다. 메시지를 먼저 보내면 반대로 실제로 확정되지 않은 주문을 다른 서비스가 처리할 수 있다.
아웃박스는 그 사이에 “보낼 일이 있다”는 영속적인 기록을 둔다. 주문과 전송할 사건을 함께 커밋하면, 주문이 확정된 뒤 전달자가 잠시 멈춰도 전송할 대상은 DB에 남아 있다. 업무 요청을 처리하는 코드의 실행 수명과 메시지 전달의 수명을 나누는 것이다.
따라서 아웃박스를 이해하는 출발점은 큐 테이블의 이름이나 주기 실행기의 문법보다 트랜잭션 경계다. 어떤 두 기록이 함께 확정되는지, 전달은 누가 맡는지, 중복 전달은 수신자가 어떻게 받아들이는지를 이어서 살펴봐야 한다.
1. 트랜잭션이 시스템 경계를 만나다
DB 내부에서 함께 확정할 수 있는 변경도 외부 메시지나 실제 행동까지 이어지면 같은 방식으로 되돌리기 어렵다. Gray의 1981년 트랜잭션 논문은 보호되는 데이터 동작과 외부 세계의 동작을 구별하며 이 경계를 논의했다.1
여러 참여자가 커밋 결정을 함께 내리는 분산 트랜잭션은 한 접근이다. 하지만 DB와 메시지 브로커가 같은 프로토콜을 지원해야 하고, 장애·대기·운영 결합도 함께 다뤄야 한다. 아웃박스는 로컬 DB에 남기는 기록을 이용해 업무 변경과 전달 사이를 연결하는 별도의 구조를 택한다.2
이 문제의 계보에는 메시징의 트랜잭션 처리도 있다. Hohpe와 Woolf의 Transactional Client 패턴은 메시지 송수신과 자원 변경의 트랜잭션 경계를 다룬다. 이런 논의가 서비스별로 데이터와 배포를 나누는 아키텍처에서 다시 구체적인 구현 문제로 나타난다.3
2. 서비스 아키텍처에서 정리된 패턴
Chris Richardson의 Transactional Outbox 설명은 서비스의 데이터 변경과 사건 발행이라는 문제, 로컬 기록과 별도 전달이라는 해법을 패턴으로 정리한다. 그의 『Microservices Patterns』는 2018년에 출판됐다.24
앞선 트랜잭션·메시징 계보에서 현재의 패턴 설명으로 이어지는 공통 질문은 서비스가 DB와 브로커에 쓰는 두 작업을 어떻게 연결할 것인가다. 최초 사용 시점은 이 자료만으로 확정하기 어렵다.
3. 기본 구조와 두 개의 저장 경계
생산자는 업무 데이터와 outbox 기록을 같은 트랜잭션에 넣는다. 커밋이 성공하면 두 기록이 함께 남고, 실패하면 둘 다 확정되지 않는다. 메시지에는 수신자가 같은 사건인지 식별할 고유 ID를 넣는다.
전달자는 커밋된 outbox 기록을 읽어 전송한다. 수신자는 자기 DB에서 업무 효과를 적용하며, 필요하면 처리한 사건 ID도 같은 트랜잭션에 기록한다. 생산자의 커밋과 수신자의 커밋 사이에는 메시지 전달이 놓여 있다.
그림의 place_order는 생산자의 주문 작업, handle_event는 수신자의 처리다. 두 DB 안의 묶음은 각자의 로컬 원자성 범위이며, 가운데 전달 동작은 별도의 단계다.
4. 전송 의도를 함께 남기는 최소 예제
아래 PostgreSQL 코드는 업무 레코드와 사건 레코드의 확정 범위만 보여준다. 전달 루프와 수신자의 상세 구현은 생략하고, 우선 어떤 두 기록을 묶는지 확인한다.
CREATE TEMP TABLE orders (id integer PRIMARY KEY, item text NOT NULL);
CREATE TEMP TABLE outbox (
event_id text PRIMARY KEY,
order_id integer NOT NULL,
event_type text NOT NULL
);
BEGIN;
INSERT INTO orders VALUES (1, 'book');
INSERT INTO outbox VALUES ('event-1', 1, 'OrderPlaced');
COMMIT;
SELECT id, item FROM orders;
SELECT event_id, order_id, event_type FROM outbox;
결과에는 주문 1과 그 주문을 가리키는 사건 event-1이 함께 남는다. 이 예제는 원자적 기록의 핵심만 나타낸다. 두 레코드를 서로 다른 DB나 별도 트랜잭션에 넣으면 같은 구조의 보장을 얻지 못한다.
5. 폴링과 로그 추적
Polling Publisher는 아직 전달하지 않은 기록을 주기적으로 조회한다. 저장소의 일반 조회로 동작을 이해하기 쉽고, 조회 주기와 배치가 전달 지연과 부하에 영향을 준다.
Transaction Log Tailing은 DB의 변경 로그를 따라 커밋된 사건을 가져간다. CDC 도구가 이 전달 경로에 쓰일 수 있다. Debezium의 Outbox Event Router는 outbox 레코드의 사건 ID, aggregate ID, payload 같은 필드를 메시지의 식별·키·본문에 연결한다.5
두 방식은 전송 대상을 발견하는 경로가 다르다. 이후 브로커에 보내고 진행 위치를 기록하는 사이의 장애는 각각의 구현과 전달 계약에서 다뤄야 한다.
6. 중복 전달 창과 수신자의 멱등성
전달자가 메시지를 보낸 직후, 전송 완료 기록을 남기기 전에 종료될 수 있다. 재시작한 전달자는 같은 사건을 다시 보낼 수 있다. 전송 성공 여부를 모르는 상황에서 기록을 먼저 완료 처리하면 메시지를 잃을 위험이 있다.6
수신자가 같은 사건 ID를 이미 처리했는지 기록하고, 그 기록과 자기 DB의 업무 효과를 함께 확정하면 중복 메시지에 대한 효과를 조절할 수 있다. 이 처리는 수신자 DB 안의 원자성에 기대며, 그 밖의 메일·결제 API에는 각자의 중복 처리 계약이 필요하다.
한 사건의 중복 전달과 한 주문 요청의 중복 접수도 다르다. 사용자가 같은 주문을 두 번 제출해 서로 다른 사건 두 개가 생겼다면, 사건 ID 중복 제거만으로 주문이 하나가 되지는 않는다.
7. 순서와 보존이 만드는 의미
같은 주문의 변경 사건은 앞뒤 순서가 중요할 수 있다. 여러 전달자가 기록을 가져갈 때 읽은 순서와 전송 완료 순서가 달라질 수 있으므로, 주문별 식별자·순번·브로커의 키·수신자 적용 규칙을 연결해야 한다.25
outbox와 수신 기록의 보존도 이 계약의 일부다. 미전송 기록을 지우면 전달할 근거가 사라지고, 처리 기록을 지운 뒤 옛 사건을 재전송하면 다시 적용될 수 있다. 보관 기간은 재전송 가능 기간과 복구 경로를 기준으로 정한다.
8. 사가·이벤트 소싱과의 관계
사가는 여러 서비스에 걸친 업무 단계를 이어가고 실패 시 보상하는 방식을 다룬다. 아웃박스는 그 단계의 결과를 사건으로 전달하는 데 쓰일 수 있다. 이벤트 소싱은 사건을 상태의 원본으로 저장하는 모델이며, outbox가 있다고 해서 업무 상태의 원본까지 사건으로 바뀌는 것은 아니다.
세 구조는 함께 사용할 수 있지만 책임은 각각 다르다. 아웃박스의 중심은 로컬 업무 변경과 전달할 기록을 함께 확정하는 데 있다.
각주
- Jim Gray, 「The Transaction Concept: Virtues and Limitations」, VLDB, 1981. 원문. ↩
- Chris Richardson, “Transactional Outbox”. 저자 패턴 설명. ↩
- Gregor Hohpe·Bobby Woolf, “Transactional Client”. 저자 패턴 설명. ↩
- Chris Richardson, 『Microservices Patterns』, Manning, October 2018. 출판사 서지. 출판 정보 확인에 사용했다. ↩
- Debezium, “Outbox Event Router”. 공식 문서. ↩
- AWS Prescriptive Guidance, “Transactional outbox pattern”. 공식 문서. ↩