Transactional Outbox 패턴: 마이크로서비스에서 신뢰성 있는 이벤트 발행하기
현대 이벤트 기반 마이크로서비스 아키텍처(Event-Driven Architecture)에서 서비스 간 상태 변경을 알리기 위해 OrderCreated나 PaymentProcessed 같은 도메인 이벤트를 발행합니다.
하지만 이벤트 발행 시 심각한 문제가 발생할 수 있습니다: 데이터베이스 업데이트와 이벤트 발행이 모두 성공하거나 모두 실패하도록 보장하는 방법은 무엇일까요?
마이크로서비스가 로컬 데이터베이스(PostgreSQL, MySQL)를 업데이트한 직후 네트워크를 통해 메시지 브로커(Apache Kafka, RabbitMQ)로 이벤트를 전송할 때 네트워크 장애나 타임아웃이 발생하면 데이터 불일치가 발생합니다.
이 문제를 해결하기 위해 Transactional Outbox 패턴을 사용합니다.
이중 쓰기 문제 (Dual-Write Problem)
서비스에서 두 번의 쓰기 작업을 연속으로 수행할 때:
- 비즈니스 데이터를 로컬 DB에 저장
- 메시지 브로커에 이벤트 발행
DB 커밋은 성공했지만 네트워크 오류로 Kafka 발행이 실패하면, DB에는 주문이 저장되었지만 하위 서비스는 이를 알지 못합니다. 결과: 데이터 불일치.
Transactional Outbox 패턴이란?
RDBMS의 로컬 ACID 트랜잭션 기능을 활용하는 패턴입니다.
이벤트를 네트워크로 직접 발행하지 않고, 비즈니스 엔티티를 저장하는 동일한 로컬 트랜잭션 내에서 Outbox 테이블에 이벤트 메시지를 함께 저장합니다.
ACID 보장에 따라 두 저장 작업이 함께 커밋되거나 함께 롤백됩니다.
이후 별도의 백그라운드 프로세스(Message Relay)가 Outbox 테이블의 이벤트를 읽어 메시지 브로커로 발행합니다.
메시지 릴레이 전략: Polling Publisher vs CDC (Debezium)
1. 폴링 방식 (Polling Publisher)
스케줄러가 주기적으로 미처리 레코드(SELECT * FROM outbox_events WHERE processed = FALSE)를 조회하여 Kafka로 전송 후 상태를 업데이트합니다.
- 장점: 구현이 간단하고 추가 인프라가 필요 없음.
- 단점: 폴링 주기만큼 지연 발생, DB 쿼리 부하.
2. CDC 방식 (Change Data Capture / Debezium)
Debezium이 데이터베이스 트랜잭션 로그(PostgreSQL WAL)를 직접 캡처하여 즉시 Kafka로 스트리밍합니다.
- 장점: 지연 시간이 거의 없고 애플리케이션 DB 부하 없음.
- 단점: Kafka Connect 및 Debezium 인프라 구축 필요.
요약
Transactional Outbox 패턴은 분산 마이크로서비스 환경에서 최종 일관성과 데이터 신뢰성을 보장하기 위한 필수 디자인 패턴입니다.