1. Transactional Outbox란
Transactional Outbox는 DB 변경과 메시지 발행 사이의 불일치를 줄이기 위한 패턴이다.
보통 서비스에서는 이런 흐름이 자주 나온다.
- DB에 데이터 저장
- 메시지 브로커에 이벤트 발행
문제는 이 둘이 하나의 원자적 작업이 아니라는 점이다.
- DB 저장은 성공
- 메시지 발행은 실패
이런 상황이 충분히 가능하다.
2. 왜 필요한가
예를 들어 새 정책을 저장한 뒤 SQS에 알림 이벤트를 보내는 구조를 생각해보자.
정책 저장
-> SQS 이벤트 발행
여기서 정책 저장은 성공했는데 SQS 발행이 실패하면 어떻게 될까?
- 정책은 DB에 남아 있음
- 알림 이벤트는 큐에 없음
- 재시도할 근거도 없음
즉 데이터는 저장됐지만, 후속 처리를 위한 메시지는 사라질 수 있다.
AFTER_COMMIT 리스너를 써도 이 문제는 해결되지 않는다. 커밋 이후의 메시지 발행은 이미 DB 트랜잭션 바깥에서 일어나기 때문이다.
3. 어떻게 해결하나
Transactional Outbox는 메시지를 바로 발행하지 않고, 먼저 DB의 outbox 테이블에 저장한다.
흐름은 이렇게 바뀐다.
비즈니스 데이터 저장
-> 같은 트랜잭션에서 outbox 저장
-> 커밋
-> relay가 outbox 조회
-> 메시지 브로커 발행
핵심은 “보내야 할 이벤트를 먼저 DB에 안전하게 남긴다”는 점이다.
그러면 메시지 브로커 발행이 실패해도 이벤트 자체는 사라지지 않는다. 나중에 relay가 다시 읽어 재시도할 수 있다.
4. 실제 적용 사례
청년 정책을 다루는 사이드 프로젝트의 정책 알림 경로에 이 패턴을 적용했다.
기존 구조는 대략 이랬다.
정책 저장
-> AFTER_COMMIT 리스너
-> 사용자 매칭
-> SQS 직접 발행
변경 후에는 이렇게 바뀌었다.
정책 저장
-> AFTER_COMMIT 리스너
-> 사용자 매칭
-> notification_outbox 저장
-> relay가 SQS 발행
이제 SQS 발행이 잠깐 실패해도 이벤트는 notification_outbox에 남아 있으므로 다시 보낼 수 있다.
5. 장점과 한계
장점은 명확하다.
- 메시지 유실 가능성을 줄일 수 있다
- 재시도 근거가 생긴다
- 운영 중 누락 추적이 쉬워진다
반대로 비용도 있다.
- outbox 테이블이 추가된다
- relay가 필요하다
- 중복 발행 가능성은 여전히 고려해야 한다
즉 Transactional Outbox는 exactly-once를 보장하는 패턴이라기보다, 실패를 복구 가능하게 만드는 패턴에 가깝다.
Transactional Outbox는 화려한 패턴은 아니지만, DB와 메시지 브로커를 함께 다루는 시스템에서는 꽤 실용적인 선택지다.