Transactional Outbox Pattern을 설명해 주세요.
이 질문은 Kafka나 메시지 큐를 이력서에 적어둔 분들에게 꽤 자주 이어질 수 있는 질문입니다.
단순히
“이벤트를 발행했습니다”
“Kafka로 비동기 처리했습니다”
라고 말하면 면접관은 보통 이렇게 물어볼 수 있습니다.
“DB 저장은 성공했는데 Kafka 발행이 실패하면 어떻게 하나요?”
“반대로 메시지는 발행됐는데 DB 트랜잭션이 롤백되면요?”
“DB와 Kafka를 하나의 트랜잭션으로 묶을 수 있나요?”
이 질문은 메시지 큐 사용 경험보다,
DB 변경과 이벤트 발행 사이의 정합성 문제를 이해하고 있는지 확인하는 질문에 가깝습니다.
같이 체크해보면 좋은 포인트는 아래와 같습니다.
DB 저장과 메시지 발행은 왜 원자적으로 묶기 어려운가?
Outbox 테이블에는 어떤 데이터를 저장해야 하는가?
이벤트 발행 실패 시 어떻게 재시도할 것인가?
같은 이벤트가 여러 번 발행되면 Consumer는 어떻게 중복 처리할 것인가?
Outbox 테이블이 계속 커질 때 어떻게 관리할 것인가?
꼬리질문으로는 이런 질문이 이어질 수 있습니다.
ㄴ> Outbox Pattern은 어떤 문제를 해결하나요?
ㄴ> Outbox 테이블에는 어떤 컬럼이 필요할까요?
ㄴ> Outbox 이벤트를 Kafka로 발행하는 방식은 Polling 방식인가요, CDC 방식인가요?
ㄴ> 메시지가 중복 발행되면 Consumer는 어떻게 멱등성을 보장하나요?
ㄴ> Outbox 테이블이 커지면 성능 저하는 어떻게 방지하나요?
ㄴ> Saga Pattern과 Outbox Pattern은 각각 어떤 문제를 해결하나요?
백엔드 면접에서 메시지 큐(Kafka, RabbitMQ 등)를 활용한 아키텍처를 언급했을 때, 꼬리 질문으로 가장 나오기 좋은 핵심 주제입니다. 질문 주신 내용들을 흐름에 따라 5가지 핵심 케이스(개념, 설계, 발행, 소비, 비교)로 나누어 명쾌하게 정리해 드리겠습니다.
COMMIT과 Kafka의 produce 요청은 하나의 트랜잭션으로 묶을 수 없습니다.이벤트의 추적성, 순서 보장, 재시도를 위해 최소한 아래와 같은 스키마 구조가 필요합니다.
| 컬럼명 | 타입 | 설명 |
|---|---|---|
| id | UUID 또는 BigInt |
이벤트를 식별하는 고유 PK (Idempotency Key로 활용 가능) |
| aggregate_type | VARCHAR |
이벤트가 발생한 도메인 영역 (예: Order, Member) |
| aggregate_id | VARCHAR |
해당 도메인의 식별자 (예: order_id) |
| event_type | VARCHAR |
이벤트 종류 (예: OrderCreated, OrderCanceled) |
| payload | TEXT 또는 JSONB |
메시지에 담을 실제 데이터 (Serialized Data) |
| status | VARCHAR |
발행 상태 관리 (PENDING, PROCESSED, FAILED) |
| created_at | TIMESTAMP |
이벤트 생성 시간 (순서 보장 및 디버깅용) |
데이터가 무한히 쌓이면 PENDING 상태를 조회하거나 인덱스를 유지하는 데 비용이 커집니다.
PROCESSED로 완료된 데이터 중, 일정 기간(예: 3일~일주일)이 지난 데이터는 스케줄러를 통해 주기적으로 하드 딜리트(Hard Delete)합니다.created_at) 기준으로 테이블을 일별/월별 파티셔닝하여, 오래된 파티션을 통째로 Drop하거나 백업 스토리지로 이관합니다.status 컬럼을 업데이트하기 위해 대량의 UPDATE 쿼리를 날릴 필요가 없어 DB 부하를 획턴하게 줄일 수 있습니다.