트랜잭셔널 아웃박스
로컬 DB 변경과 메시지 전송 사이의 실패를 다루는 아웃박스의 배경, 전달 방식, 중복·순서·보존 책임을 정리한다.
· 마지막 업데이트: 2026-11-04 · 9 min read
개요
트랜잭셔널 아웃박스(transactional outbox)는 업무 데이터의 변경과 나중에 전달할 메시지를 같은 데이터베이스 트랜잭션에 기록하는 패턴이다. 전달자는 커밋된 메시지를 별도로 읽어 브로커나 수신 서비스에 보낸다.
한 업무의 결과를 두 시스템에 나눠 남기면 그 사이에 실패 지점이 생긴다. 주문 DB를 커밋한 뒤 메시지를 보내기 전에 프로세스가 종료되면, 주문은 있지만 다른 서비스는 주문이 생겼다는 사실을 모른다. 반대로 메시지를 먼저 보내면 확정되지 않은 주문을 다른 서비스가 처리할 수 있다.
아웃박스는 그 사이에 “보낼 일이 있다”는 영속적인 기록을 둔다. 주문과 전송할 사건을 함께 커밋하면, 주문이 확정된 뒤 전달자가 잠시 멈춰도 전송할 대상은 DB에 남아 있다. 업무 요청을 처리하는 코드의 실행 수명과 메시지 전달의 수명이 여기서 나뉜다.
그래서 아웃박스를 이해하려면 큐 테이블의 이름이나 주기 실행기의 문법보다 트랜잭션 경계부터 봐야 한다. 어떤 두 기록이 함께 확정되는지, 전달은 누가 맡는지, 중복 전달을 수신자가 어떻게 받아들이는지가 모두 그 경계에서 나오는 질문이다.
연혁
1981년: Jim Gray가 VLDB에 발표한 「The Transaction Concept: Virtues and Limitations」에서 트랜잭션이 보호하는 데이터와 외부 행동의 경계를 다뤘다.1
2003년: Gregor Hohpe가 기업 통합 발표 자료에서 통합 방식을 파일 전송, 공유 데이터베이스, 원격 호출, 메시징으로 나눠 비교했다. 연도는 발표 자료의 저작권 표시이며 책의 발행연도와는 다르다.2
2018년 10월: Richardson의 『Microservices Patterns』가 Manning에서 출판됐다. 서비스별 DB 갱신과 사건 발행의 문제를 책으로 정리한 출판 기록이다.3
보호되는 데이터와 외부 행동의 경계 (1981)
Gray는 1981년 논문에서 동작을 세 가지로 나눴다. 중단이나 복구 때 되돌리거나 다시 할 필요가 없는 비보호(unprotected) 동작, 되돌리거나 다시 할 수 있고 그래야 하는 보호(protected) 동작, 한 번 하면 되돌릴 수 없는 실제(real) 동작이다. 현금 지급기 같은 장치의 실제 동작은 트랜잭션이 커밋될 때까지 미뤘다가 실행하고, 순번 같은 이름을 붙여 중복된 실행을 버린다.1
같은 트랜잭션 시스템 안에서 처리하는 메시지 동작은 Gray의 분류에서 데이터베이스 동작과 함께 보호 동작에 들어간다. 업무 DB의 트랜잭션에 참여하지 않는 브로커로 보내는 메시지는 그 로컬 복구 범위 밖에 있어서, 주문 DB를 커밋한 뒤 메시지를 보내기 전에 프로세스가 종료되는 문제도 여기서 생긴다. 두 작업을 순서대로 호출한다고 두 작업이 함께 확정되지는 않는다. 분산 트랜잭션으로 참여자들의 커밋을 조정하는 접근도 있지만, 아웃박스는 로컬에 전송 의도를 남기는 쪽으로 문제를 나눈다.14
기업 통합과 메시징의 트랜잭션 경계 (2003)
Gregor Hohpe의 2003년 발표 자료에는 애플리케이션을 연결할 때 데이터를 언제 전달하는지와 시스템끼리 얼마나 결합되는지를 함께 다뤄야 했던 배경이 드러난다. Hohpe와 Bobby Woolf의 Transactional Client 패턴은 클라이언트와 메시징 시스템의 세션을 트랜잭션으로 만들어 클라이언트가 트랜잭션 경계를 정하게 한다. 보낸 메시지는 커밋해야 채널에 들어가고, 받은 메시지는 커밋해야 채널에서 빠진다.25
이 웹 설명이 다루는 범위는 세션의 메시징 동작까지다. 별도 DB의 업무 변경까지 같은 원자성에 넣으려면 그 자원과 트랜잭션 관리자가 참여하는지를 따로 확인해야 한다. 메시징 안의 트랜잭션과 서비스 DB의 트랜잭션은 서로 다른 경계일 수 있고, 그 사이에 놓인 아웃박스는 업무를 확정했다는 사실을 나중에 전달할 기록으로 남긴다.
서비스별 DB와 사건 발행의 패턴화 (2018)
Chris Richardson은 2018년 10월 출판된 『Microservices Patterns』에서 서비스별 DB 갱신과 사건 발행의 문제를 다뤘다. 그의 현재 패턴 카탈로그는 업무 데이터와 발행할 사건을 같은 로컬 트랜잭션에 기록하는 방식을 Transactional Outbox라는 이름으로 설명하고, 다른 이름으로 Application events를 병기한다. 전달자는 커밋된 사건을 따로 읽어 브로커에 보낸다. 업무 요청의 실행이 끝나도 전송할 일은 DB에 남아 있으므로 전달을 나중에 이어갈 수 있다.43
이 구조에서 함께 확정되는 것은 생산자 DB의 두 기록뿐이고, 한 번의 커밋이 묶는 범위도 거기까지다. 수신자는 자기 DB의 상태를 별도로 확정하며, 브로커는 그 사이에서 메시지를 옮긴다.
테이블 폴링과 로그 기반 전달
Richardson의 카탈로그는 전달자를 구현하는 방식으로 폴링과 트랜잭션 로그 추적을 구분한다. 폴링은 커밋된 outbox 행을 조회해서, 로그 추적은 DB가 남긴 변경 기록을 읽어서 전송 대상을 찾는다.4
Debezium의 Outbox Event Router는 변경 데이터 캡처(CDC)로 얻은 outbox 레코드를 이벤트로 라우팅하는 구체적인 도구다. 기본 설정에서 사건 ID(id)는 메시지 헤더에, 집계 대상 식별자(aggregateid)는 메시지 키에 들어가고, 집계 대상 타입(aggregatetype)은 토픽 이름에 쓰이며, 페이로드는 메시지 값이 된다. 사건 타입 같은 다른 열은 table.fields.additional.placement 설정으로 헤더나 엔벨로프에 추가한다.6
폴링은 조회 주기·인덱스·작업 선점의 영향을 받고, 로그 기반 전달은 로그 보존·체크포인트·커넥터의 수명을 함께 관리한다. 어느 쪽이든 전달자가 중간에 멈췄을 때 다시 읽기 시작할 위치와 기록이 필요하다.
중복 전달과 멱등한 수신
전달자가 메시지를 보낸 뒤 “보냈다”는 상태를 기록하기 전에 종료되면 같은 사건을 다시 보낼 수 있다. AWS의 패턴 설명도 중복 전달 가능성과 수신자의 멱등성 문제를 함께 다룬다.7
수신자는 사건 ID를 보고 이미 처리한 사건인지 구별할 수 있다. 처리한 ID와 업무 효과를 같은 로컬 트랜잭션에 넣어 두면, 재전달된 사건이 업무 효과를 다시 만드는 일을 막을 수 있다. 사건 ID의 범위와 보존 기간도 이 중복 판별 계약에 포함된다.
이 중복 방지가 미치는 범위는 같은 DB에 함께 기록한 효과까지다. 외부 결제나 메일처럼 다른 시스템에 남는 효과가 있다면, 그 시스템과도 요청을 식별하고 처리 결과를 주고받는 계약이 필요하다.
순서, 보존, 다른 패턴과의 경계
같은 주문의 생성과 취소처럼 순서에 따라 의미가 달라지는 사건은 전달 순서를 설계해야 한다. 수신자를 병렬화할 때도 어느 집계 대상 안에서 순서를 지킬지 정해야 하는데, 메시지의 키와 파티션이 이를 구현하는 수단이다.46
사건 기록을 언제 지우는지가 전달자의 진행 위치와 복구 가능성을 좌우한다. 커밋된 사건을 전달 전에 지우면 이어갈 기록이 사라지고, 전달한 뒤에도 계속 쌓아 두면 저장 비용이 커진다. 삭제 조건은 확인 지점과 복구 기간에 맞춰 정한다.
아웃박스는 상태 변경의 전달을 맡고, 사가는 여러 서비스의 업무 단계를 조정한다. 이벤트 소싱에서는 사건 자체가 업무 상태의 원본이다. 이 구분은 메시지를 보관하는 테이블의 유무가 아니라, 어느 기록을 상태의 근거로 삼는지에 달려 있다.
두 저장 경계 예제
그림은 생산자와 수신자의 로컬 트랜잭션을 각각 묶어 보여준다. 브로커 없이 전달 코드가 수신자를 직접 호출하는 경우라서 sent는 수신자가 처리한 뒤에 표시되며, 브로커를 두면 전달자가 브로커의 수신 확인을 받은 뒤 sent를 표시한다. 아래 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;
두 SELECT에는 주문 1건과 event-1 사건 1건이 나온다. BEGIN과 COMMIT은 이 두 기록을 함께 확정한다. 그림의 handle_event와 수신자 DB는 그 뒤의 별도 처리 경계다. 예제의 TEMP 테이블은 만든 세션에서만 보이는 실습용이며, 실제로는 전달자 세션도 읽을 수 있는 일반 테이블이어야 한다.
각주
- Jim Gray, 「The Transaction Concept: Virtues and Limitations」, VLDB, 1981. 원문. ↩
- Gregor Hohpe, 「Enterprise Integration Patterns」 발표 자료, 2003, 통합 방식과 메시징 관련 슬라이드. 저자 공개 자료. 연도는 이 발표 자료의 저작권 표시이며 책의 발행연도를 뜻하지 않는다. ↩
- Chris Richardson, 『Microservices Patterns』, Manning, October 2018. 출판사 서지. 출판 정보 확인에 사용했다. ↩
- Chris Richardson, “Transactional Outbox”. 저자 패턴 설명. ↩
- Gregor Hohpe·Bobby Woolf, “Transactional Client”. 저자 패턴 설명. ↩
- Debezium, “Outbox Event Router”. 공식 문서. ↩
- AWS Prescriptive Guidance, “Transactional outbox pattern”. 공식 문서. ↩