- Transactional Outbox 패턴은 실제 테이블을 업데이트하는 행위와 이벤트를 발생하는 행위 사이에 일관성을 보장하기 위한 패턴이다.
- 데이터베이스와 메시지 브로커가 일관성이 유지되지 않는다면, 실제 데이터의 정합성이 깨질 것이기 때문이다.
- 그렇다면 서비스가 데이터베이스를 업데이트하면서 이벤트 메시지를 전송할 때, 두 작업을
원자적으로 처리하는 방법은 무엇일까?
- 그렇다면 이벤트 자체를 데이터베이스에 저장하는 방식이다. →
Outbox Table
- DBMS에서 실제 엔터티의 저장과 Outbox의 저장을
원자적으로 처리함으로써, 이벤트와 엔터티의 일관성이 보장될 수 있다.
- 후에 이 이벤트를 별도의 프로세스 (이벤트 릴레이)가 메시지 브로커로 전송하여 처리하는 방법으로 처리하는 것이다.
- 이로 인한 얻게되는 장점 중 하나는 메시지 발행 로직을 트랜잭션에서 분리하기에
메시지 브로커와의 의존성을 제거할 수 있다는 점이다.
- 이때,
Outbox테이블을 읽는 방식에는 폴링과 log를 읽는 방식으로 크게 존재하는데, 이전에 근무했던 회사에서는 MongoDB의 Change Stream을 사용하였다.
- Change Stream은 log를 통해 전달하는 방식
- 추가적인 저장공간이 필요하다는 점말고는 구현도 간단하며, 일관성 보장 측면에서는 굉장히 뛰어난 패턴이라고 생각이 든다.
- 물론, 추가적인 프로세스가
Outbox를 주기적으로 읽고 메시지를 보내는 방식이기에 최종적 일관성이 보장은 되지만 그 텀이 조금 길 수도 있을 것 같다.

예시
- 간단한 예시를 들어보자면 다음과 같다.
- 예를 들어, 주문을 하는 시스템에서 주문을 생성할 때를 생각해 본다면
- 주문 생성 트랜잭션
- 주문 정보를 데이터베이스에 저장하면서, Outbox 테이블에 주문 생성 이벤트를 기록한다. 이 두 작업은 하나의 트랜잭션으로 처리.
- 이벤트 전달
- Outbox 테이블을 주기적으로 읽는 프로세스가 주문 생성 이벤트를 메시지 브로커에 전달한다.
- 전송 확인
- 메시지가 전송되면 Outbox 테이블의 해당 이벤트를 삭제하거나 처리 상태를 업데이트.
- 이와 같은 방식을 통해 주문 정보와 주문 생성 이벤트 간의 일관성이 보장되며, 메시지가 전송되지 않는 문제가 생기더라도
Outbox테이블에서 데이터를 보고 이벤트를 재전송 할 수도 있다.
남아있는 문제
이벤트의 중복
- Outbox 테이블을 읽는 프로세스를
메시지 릴레이라고 부르는데, 해당 서비스가 메시지 브로커에 전달하다가 오류가 나서
- 다른 메시지 릴레이 서비스의 인스턴스가
Outbox테이블에서 다시 메시지 브로커에 전달하는 경우 같은 메시지가 두번 전달되는 상황이 벌어진다.
- 이 문제는 Outbox 테이블에 메시지 정보를 저장할 때, 각 메시지마다 ID값을 할당하고, 메시지 브로커로 전달하는 이벤트에 해당 아이디 값을 부여하면 쉽게 해결할 수 있다.
데이터베이스가 트랜잭션을 미지원
- NoSQL DB같은 DBMS는 트랜잭션을 지원하지 않는다.
- 정확하게는 최근에는 대부분 지원하지만 성능이 좋지 않다.
- Transaction Outbox 패턴의 핵심 개념은 엔터티와 Outbox의 원자성을 보장하여 이벤트 발행과 데이터의 변화 사이의 트랜잭션을 보장하는 것인데, 이 부분이 불가능하다.
- 그렇다면 어떻게 해결할 수 있을까?
- 해결 방법은 간단하다. 단순히 데이터 상태를 직접 업데이트 하지 않고, 변경할 상태를 이벤트로 기록하는 것이다.
- 해당 변경사항을 이벤트로 전달하여 간접적으로 데이터를 업데이트 하는 것이다.
메시지 순서
- 만약 사용자가 상품을 주문하자마자 바로 취소하는 경우에는 메시지 릴레이가 생성과 취소 두개의 메시지를 발견하였고,
- 비즈니스 순서를 이해하지 못하고 주문 취소 이벤트를 주문 생성 이벤트보다 먼저 발행할 수도 있다.
- 이러한 경우에는,
Outbox테이블에서 각 메시지마다 연속적인 아이디 혹은 타임스탬프를 부여해서, 이벤트를 발행할때 정렬하는 방식으로 순서를 맞출 수 있다.