Outbox 패턴이란 무엇인가
마이크로서비스 환경에서는 데이터베이스에 상태를 저장함과 동시에, 다른 서비스에 변경 사실을 알리는 이벤트를 메시지 브로커(Kafka, RabbitMQ 등)로 발행하는 경우가 많습니다. 문제는 ‘DB 저장’과 ‘이벤트 발행’이 서로 다른 시스템에 대한 별개의 작업이라는 점입니다. DB 커밋은 성공했는데 이벤트 발행이 실패하거나, 반대로 이벤트는 나갔는데 DB 커밋이 롤백되면 데이터 정합성이 깨집니다. 이를 ‘이중 쓰기(Dual Write) 문제’라고 부릅니다. Outbox 패턴은 이 문제를 해결하기 위해, 이벤트를 즉시 브로커로 보내지 않고 같은 DB 트랜잭션 안에 있는 ‘outbox’라는 별도 테이블에 먼저 기록하는 방식입니다.
동작 원리: 하나의 트랜잭션으로 묶기
예를 들어 주문 서비스가 주문을 생성한다고 가정해봅시다. 이때 다음 두 작업을 하나의 트랜잭션으로 처리합니다.
- orders 테이블에 주문 데이터 저장
- outbox 테이블에 ‘주문생성’ 이벤트 데이터 저장
두 작업이 같은 트랜잭션이므로 둘 다 성공하거나 둘 다 실패합니다. 이후 별도의 프로세스(릴레이 또는 메시지 릴레이어)가 주기적으로 outbox 테이블을 폴링하거나, Debezium 같은 CDC(Change Data Capture) 도구가 트랜잭션 로그를 감지해 outbox에 쌓인 이벤트를 읽어 메시지 브로커로 발행합니다. 발행에 성공하면 해당 레코드를 ‘전송완료’로 표시하거나 삭제합니다. 이렇게 하면 DB 트랜잭션과 이벤트 발행이 논리적으로 하나의 원자적 작업처럼 동작하게 됩니다.
비유하자면 택배 접수와 비슷합니다. 접수 직원이 물건을 받을 때마다 택배기사에게 바로 전화하는 대신, 접수대장(outbox)에 기록만 해두고 별도 담당자가 대장을 순서대로 확인하며 기사에게 연락하는 것과 같습니다. 접수(트랜잭션)와 대장 기록은 동시에 이루어지므로 누락될 일이 없고, 연락은 나중에 안정적으로 처리됩니다.
실무에서 왜 필요한가
Outbox 패턴은 ‘적어도 한 번(at-least-once)’ 이벤트 전달을 보장하면서도 DB 트랜잭션의 원자성을 그대로 활용할 수 있다는 장점이 있습니다. 분산 트랜잭션(2PC)처럼 복잡하고 성능이 떨어지는 방식 없이도 정합성을 지킬 수 있어, 주문·결제·재고 처리처럼 데이터 정합성이 중요한 시스템에서 널리 사용됩니다. 다만 이벤트가 중복 전송될 수 있으므로 소비자 측에서는 멱등성(idempotency) 처리를 함께 구현하는 것이 일반적입니다.