Transactional Outbox 패턴으로 메시지 유실 막기

Jinpyo·2026년 1월 20일

Server Infrastructure

분산 시스템에서 DB 저장과 메시지 발행을 동시에 처리할 때 흔히 발생하는 문제가 있다.

@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);           // ✅ 성공
    kafkaTemplate.send("orders", order);   // ❌ 실패하면?
}

DB는 저장됐는데 Kafka 발행이 실패하면? 주문은 생성됐지만 결제 서비스는 이 사실을 모른다. 데이터 정합성이 깨지는 순간이다.


해결: Outbox 테이블

핵심 아이디어는 간단하다. 메시지를 브로커에 직접 보내지 말고 DB에 먼저 저장하는 것이다.

CREATE TABLE outbox_events (
    id BIGSERIAL PRIMARY KEY,
    aggregate_type VARCHAR(100),
    aggregate_id VARCHAR(100),
    payload JSONB,
    status VARCHAR(20) DEFAULT 'PENDING',
    created_at TIMESTAMP DEFAULT NOW()
);
@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);
    outboxRepository.save(new OutboxEvent("Order", order.getId(), toJson(order)));
    // 같은 트랜잭션 = 둘 다 성공 or 둘 다 롤백
}

별도 스케줄러가 PENDING 상태를 폴링해서 브로커로 발행한다.

@Scheduled(fixedDelay = 100)
public void publish() {
    outboxRepository.findByStatus("PENDING").forEach(e -> {
        kafkaTemplate.send(e.getType(), e.getPayload());
        e.setStatus("PUBLISHED");
    });
}

핵심 포인트

  • DB 롤백 시 outbox도 롤백 → 유령 메시지 방지
  • 발행 실패해도 outbox에 남아있어 재시도 가능
  • 폴링 지연이 문제라면 Debezium CDC 도입 고려

참고: 분산 시스템에서의 데이터 무결성 검증

0개의 댓글