백엔드 면접 용어 사전
메시지 큐

dual-write 문제

DB와 브로커에 각각 쓰다 한쪽만 성공해 불일치가 생기는 문제. Outbox 패턴으로 해결한다.

DB와 메시지 브로커 두 곳에 각각 써야 하는데, 한쪽만 성공해서 상태가 어긋나는 문제.

어떻게 깨지는가

@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);        // ① DB
    kafkaTemplate.send("orders", event); // ② 브로커
}

경우 1 — ①은 성공, ②는 실패

  • DB에는 주문이 있는데 이벤트가 안 나감
  • 재고 차감·알림·정산이 전부 누락. 아무도 모른다

경우 2 — 순서를 바꿔도 마찬가지

  • 이벤트는 나갔는데 DB 저장 실패
  • 존재하지 않는 주문에 대한 이벤트가 떠다님. 하위 시스템이 오염된다

경우 3 — 가장 헷갈리는 것

  • send()는 성공했는데 그 뒤 트랜잭션이 롤백
  • DB에는 없는데 이벤트만 나감 @Transactional 안에서 send를 불러도 브로커는 트랜잭션의 일부가 아니다. 롤백해도 이미 나간 메시지는 되돌아오지 않는다.

왜 트랜잭션으로 못 묶나

DB와 브로커는 서로 다른 시스템이다. 하나의 트랜잭션에 넣으려면 분산 트랜잭션(2PC)이 필요한데,

  • 코디네이터가 단일 장애점이 되고
  • 참여자가 준비 상태로 락을 오래 쥐며
  • Kafka는 XA를 지원하지 않는다

그래서 실무에서는 2PC를 쓰지 않는다.

해결 — Transactional Outbox

브로커에 직접 쓰지 않고, 같은 DB의 outbox 테이블에 쓴다. 트랜잭션이 하나뿐이므로 어긋날 수 없다. 별도 프로세스가 그 테이블을 읽어 발행한다.

흔한 임시방편과 그 한계

@TransactionalEventListener(phase = AFTER_COMMIT)
public void publish(OrderCreatedEvent e) { kafka.send(...); }

커밋 이후에 발행하므로 경우 3은 막힌다. 하지만 커밋 직후 애플리케이션이 죽으면 이벤트가 영영 안 나간다(경우 1). 유실을 허용할 수 있는 알림 같은 데는 충분하지만, 정산·재고처럼 놓치면 안 되는 것에는 Outbox가 필요하다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 메시지 큐·비동기 — Kafka/RabbitMQ·EOS·순서·outbox