AI 요약
주문을 DB에 저장한 뒤 곧바로 Kafka에 이벤트를 발행할 때 서버가 죽으면 DB에는 남고 Kafka에는 전달되지 않는 문제가 생기는데, 이는 DB와 Kafka가 같은 트랜잭션으로 묶일 수 없기 때문입니다. Outbox 패턴은 같은 DB 트랜잭션 안에 이벤트를 기록해 별도 Publisher가 나중에 Kafka로 발행하게 함으로써 이벤트 유실을 줄이되, 중복 처리(멱등성)는 여전히 고려해야 한다는 현실적인 해결책을 제시합니다.
Kafka를 공부하다 보면 이런 의문이 생긴다.
주문을 저장한 뒤 Kafka에 ORDER_CREATED 이벤트를 발행한다고 해보자.
주문 DB 저장
Kafka 이벤트 발행
겉으로 보면 별 문제 없어 보이지만, 1번이 성공한 직후 서버가 죽으면 문제가 생긴다.
DB에는 주문이 존재하지만 Kafka Consumer들은 주문이 생성됐다는 사실을 알 수 없다.
문제는 DB와 Kafka가 서로 다른 시스템이라는 점이다.
DB 트랜잭션 하나로 둘을 간단하게 묶을 수 없다.
이 문제를 해결할 때 사용할 수 있는 패턴이 Outbox 패턴이다.
처음에는 Outbox라는 별도의 메시지 시스템이 있는 줄 알았는데, 실제로는 단순한 DB 테이블이다.
주문을 저장할 때 Kafka에 바로 메시지를 보내는 대신, 같은 DB 트랜잭션 안에서 Outbox에도 이벤트를 저장한다.
01@Transactional 02public void createOrder() { 03 // 생략04 orderRepository.save(order); 05 outboxRepository.save(orderCreatedEvent); 06}둘은 같은 DB에 있으므로 하나의 트랜잭션으로 묶을 수 있다.
둘 중 하나라도 실패하면 같이 롤백된다.
이후 별도의 Publisher가 Outbox의 주문완료 이벤트를 찾아 Kafka로 발행한다.
Kafka 발행이 성공하면 Outbox 이벤트를 변경한다.
이제 주문 저장 직후 서버가 죽더라도 Outbox 데이터는 DB에 남아 있게 되었다.
서버가 다시 살아나면 Publisher가 아직 주문완료가 안된 이벤트를 찾아 Kafka에 다시 발행하면 된다.
즉 핵심은 Kafka 발행 자체를 DB 트랜잭션에 넣는 게 아니라, Kafka에 발행해야 한다는 사실을 DB 트랜잭션에 저장하는 것이다.
Outbox를 사용한다고 모든 문제가 끝나는 것은 아니다.
발행 성공을 하더라도 주문완료 처리 전에 서버에 장애가 발생하면 Outbox에는 여전히 주문완료가 안된 상태로 남아 있기 때문에 같은 이벤트가 다시 발행될 수 있다.
그래서 Consumer도 같은 이벤트를 여러 번 받아도 문제가 없도록 멱등성을 고려해야 한다.
예를 들어 DB에 event_id를 저장해 이미 처리한 이벤트라면 무시하는 방식이다.
결국 Outbox 패턴은 메시지 중복을 완전히 없애는 기술이 아니다.
DB 변경과 이벤트 발행 사이에서 이벤트를 잃어버리지 않도록 만드는 방법에 가깝다.
Kafka 같은 메시지 시스템을 사용할 때는 단순히 메시지를 보내는 것보다
DB 저장과 메시지 발행 사이에서 장애가 나면 어떻게 될까?
를 먼저 생각하자.