Outbox 패턴과 메시지 전송
전송할 메시지를 업무 데이터와 같은 트랜잭션으로 테이블에 기록하고, 전송은 별도 작업에 위임한다.
저장과 전송의 불일치
주문을 DB에 저장하는 작업과 브로커로 메시지를 전송하는 작업은 한 트랜잭션으로 묶이지 않는다. 저장한 뒤 전송에 실패하면 메시지가 누락된다. 먼저 전송한 뒤 저장이 롤백되면 존재하지 않는 주문의 메시지가 전송된다.
동일 트랜잭션의 Outbox 행 삽입
transactional outbox 패턴은 메시지를 즉시 전송하지 않고, 전송할 내용을 outbox 테이블에 행으로 기록하는 패턴이다. 주문 행과 outbox 행이 같은 트랜잭션에 속하므로 두 행이 모두 저장되거나 모두 저장되지 않는다.
CREATE TABLE outbox (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
topic text NOT NULL,
payload jsonb NOT NULL,
sent_at timestamptz
);
BEGIN;
INSERT INTO orders (item, qty) VALUES ('A-100', 2);
INSERT INTO outbox (topic, payload)
VALUES ('order.created', '{"item": "A-100", "qty": 2}');
COMMIT;
별도 작업의 메시지 전송
relay라고 하는 별도 작업이 아직 전송하지 않은 행을 읽어 브로커로 전송하고, 전송에 성공한 행에 전송 시각을 기록한다.
BEGIN;
SELECT id, topic, payload FROM outbox
WHERE sent_at IS NULL
ORDER BY id
LIMIT 50
FOR UPDATE SKIP LOCKED;
-- 읽은 행을 브로커로 보낸 뒤, 성공한 id만 표시한다
UPDATE outbox SET sent_at = now() WHERE id IN (1, 2, 3);
COMMIT;
FOR UPDATE SKIP LOCKED는 다른 트랜잭션이 잠근 행의 잠금 해제를 대기하지 않고 그 행을 제외한다. 따라서 relay를 여러 개 실행해도 같은 행을 중복해서 선택하지 않는다. 다만 여러 relay가 분담하여 전송하면 메시지 순서가 뒤바뀔 수 있다.
수신 측의 중복 처리
relay가 메시지를 전송한 뒤 sent_at을 기록하기 전에 종료되면 그 행은 다시 전송된다. 따라서 outbox의 전달 보장은 at-least-once(최소 한 번)이다. 메시지에 outbox id를 포함하여 전송하고, 수신 측은 이미 처리한 id를 무시한다.