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