트랜잭셔널 아웃박스 패턴에 대해서 설명해주세요

트랜잭셔널 아웃박스 패턴은 분산 시스템에서 데이터베이스 쓰기와 메시지 발행을 함께 처리할 때 발생하는 이중 쓰기 문제를 줄이기 위한 Outbox 패턴입니다.

이중 쓰기 문제

다음처럼 DB 저장과 이벤트 발행을 한 메서드에서 처리한다고 가정합니다.

@Transactional
public void propagateSample() {
    Product product = new Product("신규 상품");
    productRepository.save(product);
    eventPublisher.propagate(new NewProductEvent(product.getId()));
}

이 코드는 하나의 흐름처럼 보이지만 실제로는 DB와 메시지 브로커라는 서로 다른 시스템에 쓰는 작업입니다. 트랜잭션은 커밋됐지만 이벤트 발행이 실패하거나, 이벤트 발행은 성공했지만 커밋이 실패하면 데이터 정합성 문제가 생길 수 있습니다.

해결 방식

이벤트를 외부 시스템에 바로 발행하지 않고, 같은 DB 트랜잭션 안에서 Outbox 테이블에 저장합니다.

@Transactional
public void propagateSample() {
    Product product = new Product("신규 상품");
    productRepository.save(product);
    productOutboxRepository.save(new ProductEvent(product.getId()));
}

이렇게 하면 상품 저장과 이벤트 저장이 같은 트랜잭션에 묶입니다. 둘 다 성공하거나 둘 다 실패하므로 원자성을 보장할 수 있습니다.

이벤트 발행

트랜잭션 커밋 이후 별도 프로세스가 Outbox 테이블의 미발행 이벤트를 조회해 메시지 브로커로 발행합니다.

비즈니스 트랜잭션
  ├─ 상품 저장
  └─ Outbox 이벤트 저장

별도 발행 프로세스
  ├─ Outbox 조회
  ├─ Kafka 등으로 발행
  └─ 발행 완료 상태 변경

발행 방식은 스케줄러가 주기적으로 조회하는 Polling 방식이나, CDC로 테이블 변경을 감지하는 방식이 있습니다.

주의사항

Outbox 패턴은 메시지 유실을 줄이지만 중복 발행 가능성은 남습니다. 예를 들어 메시지 발행은 성공했는데 Outbox 상태 변경 전에 장애가 나면, 재시도 과정에서 같은 이벤트가 다시 발행될 수 있습니다. 따라서 Consumer는 멱등성을 보장해야 합니다.

정리

트랜잭셔널 아웃박스 패턴은 DB 저장과 이벤트 발행 준비를 같은 트랜잭션 안에서 처리해 이중 쓰기 문제를 줄입니다. 실제 메시지 발행은 별도 프로세스가 Outbox 테이블을 읽어 수행하며, 중복 발행에 대비한 멱등 처리가 필요합니다.

추가 학습 자료

CSLOG에서 보기