IT/CS 공부

[CS] 트랜잭셔널 아웃박스 패턴 (Transactional Outbox Pattern)

박소민 2025. 6. 17. 15:27
트랜잭셔널 아웃박스 패턴 (Transactional Outbox Pattern)
분산 서버(MSA) 구조에서
데이터 저장과 이벤트 발행을 하나의 트랜잭션으로 처리하기 위해 이벤트를 DB에 먼저 저장하고,
별도 프로세스에서 실제 메시지를 발행하는 방식

 

1. 문제 상황: 이중 쓰기(Double Write) 문제
  • 서비스 로직에서 DB에 데이터를 저장한 후, 메시지 브로커(Kafka, RabbitMQ 등)에 이벤트를 발행하는 구조에서는
  • 이중 쓰기 문제가 발생할 수 있음.
    • DB 저장은 성공했지만 이벤트 발행이 실패할 수 있음
    • 반대로 이벤트 발행은 성공했지만 DB 트랜잭션이 롤백될 수도 있음
    • 이로 인해 시스템 간 데이터 불일치 발생 가능
@Transactional
public void propagateSample() {
    Product product = new Product("신규 상품");
    productRepository.save(product);
    eventPublisher.propagate(new NewProductEvent(product.getId()));
}

 

 

2. 해결 방법: 트랜잭셔널 아웃박스 패턴
  • DB와 메시지 브로커에 동시에 쓰지 않고,
  • 이벤트를 Outbox 테이블에 먼저 저장한 후, 별도 프로세스가 해당 테이블을 읽어 이벤트를 발행하는 구조
    • ProductOutboxEvent는 Outbox 테이블의 엔티티
    • DB 트랜잭션 내에서 상품 정보와 이벤트 정보를 함께 저장
// 서비스 트랜잭션 내에서 처리

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

    ProductOutboxEvent event = new ProductOutboxEvent(product.getId(), "NEW_PRODUCT", LocalDateTime.now(), false);
    productOutboxRepository.save(event);
}

 

 

3. 이벤트 발행 프로세스: 별도 프로세스에서 처리
  • DB에 저장된 이벤트를 주기적으로 조회해 메시지 브로커로 발행하고, 처리 상태를 업데이트
    • findUnpublishedEvents()는 아직 전송되지 않은 이벤트를 조회
    • 메시지 발행 후 성공 시 published 플래그를 true로 변경
    • 실패 시 롤백 없이 레코드를 남겨 재시도 가능
// 스케줄러 기반

@Scheduled(fixedDelay = 1000)
public void publishOutboxEvents() {
    List<ProductOutboxEvent> events = productOutboxRepository.findUnpublishedEvents();

    for (ProductOutboxEvent event : events) {
        try {
            eventPublisher.publish(new NewProductEvent(event.getProductId()));
            event.markPublished();
            productOutboxRepository.save(event);
        } catch (Exception e) {
            // 로깅 또는 재시도 로직
        }
    }
}

 

장점
  • DB 저장과 이벤트 저장이 동일 트랜잭션으로 원자성 보장
  • 메시지 발행 실패 시에도 이벤트 정보 유실 없음
  • 재시도를 통해 안정성 확보
  • 시스템 간 데이터 정합성 유지 가능