−트랜잭셔널 아웃박스(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년에 출판됐다.[각주 2][각주 4]
앞선 트랜잭션·메시징 계보에서 현재의 패턴 설명으로 이어지는 공통 질문은 서비스가 DB와 브로커에 쓰는 두 작업을 어떻게 연결할 것인가다. 최초 사용 시점은 이 자료만으로 확정하기 어렵다.
3. 기본 구조와 두 개의 저장 경계
생산자는 업무 데이터와 outbox 기록을 같은 트랜잭션에 넣는다. 커밋이 성공하면 두 기록이 함께 남고, 실패하면 둘 다 확정되지 않는다. 메시지에는 수신자가 같은 사건인지 식별할 고유 ID를 넣는다.
전달자는 커밋된 outbox 기록을 읽어 전송한다. 수신자는 자기 DB에서 업무 효과를 적용하며, 필요하면 처리한 사건 ID도 같은 트랜잭션에 기록한다. 생산자의 커밋과 수신자의 커밋 사이에는 메시지 전달이 놓여 있다.
[SVG 다이어그램]
그림의 place_order는 생산자의 주문 작업, handle_event는 수신자의 처리다. 두 DB 안의 묶음은 각자의 로컬 원자성 범위이며, 가운데 전달 동작은 별도의 단계다.
4. 전송 의도를 함께 남기는 최소 예제
아래 PostgreSQL 코드는 업무 레코드와 사건 레코드의 확정 범위만 보여준다. 전달 루프와 수신자의 상세 구현은 생략하고, 우선 어떤 두 기록을 묶는지 확인한다.
+트랜잭셔널 아웃박스(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를 커밋한 뒤 메시지를 보내기 전에 프로세스가 종료되는 문제도 여기서 생긴다. 두 작업을 순서대로 호출한다고 두 작업이 함께 확정되지는 않는다. 분산 트랜잭션으로 참여자들의 커밋을 조정하는 접근도 있지만, 아웃박스는 로컬에 전송 의도를 남기는 쪽으로 문제를 나눈다.[각주 1][각주 4]
기업 통합과 메시징의 트랜잭션 경계 (2003)
Gregor Hohpe의 2003년 발표 자료에는 애플리케이션을 연결할 때 데이터를 언제 전달하는지와 시스템끼리 얼마나 결합되는지를 함께 다뤄야 했던 배경이 드러난다. Hohpe와 Bobby Woolf의 Transactional Client 패턴은 클라이언트와 메시징 시스템의 세션을 트랜잭션으로 만들어 클라이언트가 트랜잭션 경계를 정하게 한다. 보낸 메시지는 커밋해야 채널에 들어가고, 받은 메시지는 커밋해야 채널에서 빠진다.[각주 2][각주 5]
이 웹 설명이 다루는 범위는 세션의 메시징 동작까지다. 별도 DB의 업무 변경까지 같은 원자성에 넣으려면 그 자원과 트랜잭션 관리자가 참여하는지를 따로 확인해야 한다. 메시징 안의 트랜잭션과 서비스 DB의 트랜잭션은 서로 다른 경계일 수 있고, 그 사이에 놓인 아웃박스는 업무를 확정했다는 사실을 나중에 전달할 기록으로 남긴다.
서비스별 DB와 사건 발행의 패턴화 (2018)
Chris Richardson은 2018년 10월 출판된 『Microservices Patterns』에서 서비스별 DB 갱신과 사건 발행의 문제를 다뤘다. 그의 현재 패턴 카탈로그는 업무 데이터와 발행할 사건을 같은 로컬 트랜잭션에 기록하는 방식을 Transactional Outbox라는 이름으로 설명하고, 다른 이름으로 Application events를 병기한다. 전달자는 커밋된 사건을 따로 읽어 브로커에 보낸다. 업무 요청의 실행이 끝나도 전송할 일은 DB에 남아 있으므로 전달을 나중에 이어갈 수 있다.[각주 4][각주 3]
이 구조에서 함께 확정되는 것은 생산자 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에 함께 기록한 효과까지다. 외부 결제나 메일처럼 다른 시스템에 남는 효과가 있다면, 그 시스템과도 요청을 식별하고 처리 결과를 주고받는 계약이 필요하다.
순서, 보존, 다른 패턴과의 경계
같은 주문의 생성과 취소처럼 순서에 따라 의미가 달라지는 사건은 전달 순서를 설계해야 한다. 수신자를 병렬화할 때도 어느 집계 대상 안에서 순서를 지킬지 정해야 하는데, 메시지의 키와 파티션이 이를 구현하는 수단이다.[각주 4][각주 6]
사건 기록을 언제 지우는지가 전달자의 진행 위치와 복구 가능성을 좌우한다. 커밋된 사건을 전달 전에 지우면 이어갈 기록이 사라지고, 전달한 뒤에도 계속 쌓아 두면 저장 비용이 커진다. 삭제 조건은 확인 지점과 복구 기간에 맞춰 정한다.
아웃박스는 상태 변경의 전달을 맡고, 사가는 여러 서비스의 업무 단계를 조정한다. 이벤트 소싱에서는 사건 자체가 업무 상태의 원본이다. 이 구분은 메시지를 보관하는 테이블의 유무가 아니라, 어느 기록을 상태의 근거로 삼는지에 달려 있다.
두 저장 경계 예제
그림은 생산자와 수신자의 로컬 트랜잭션을 각각 묶어 보여준다. 브로커 없이 전달 코드가 수신자를 직접 호출하는 경우라서 sent는 수신자가 처리한 뒤에 표시되며, 브로커를 두면 전달자가 브로커의 수신 확인을 받은 뒤 sent를 표시한다. 아래 PostgreSQL 코드는 그중 생산자의 주문과 사건 기록만 실행한다. 전달자와 수신자 코드는 이 예제의 범위 밖이다.
[SVG 다이어그램]