프로그래밍/디자인패턴

트랜잭셔널 아웃박스 패턴(Transactional Outbox Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 20:24

"회원 가입(INSERT)은 성공했는데, 가입 축하 메일 이벤트(send) 발송은 네트워크 오류로 실패했습니다. DB는 롤백해야 할까요?"

DB 트랜잭션과 외부 메시지 큐(Kafka, RabbitMQ) 발행은 하나의 트랜잭션으로 묶을 수 없습니다(Dual Write Problem).

이를 해결하기 위해 **"보내야 할 메시지도 일단 DB에 같이 저장(Commit)하고, 나중에 꺼내서 보내자"**는 것이 아웃박스 패턴입니다.

핵심은 "DB에 저장된 메시지를 어떻게 꺼내서 브로커로 보낼 것인가?" 입니다.


1. 방식 1: 폴링 퍼블리셔 (Polling Publisher) - "심플한 스케줄러"

가장 직관적이고 구현하기 쉬운 방식입니다. 별도의 테이블(outbox)에 이벤트를 저장하고, 스케줄러가 주기적으로 이 테이블을 조회해서 메시지를 보냅니다.

동작 순서

  1. 트랜잭션 범위 내:
    • 비즈니스 데이터 저장 (INSERT INTO members ...)
    • 발송할 이벤트 저장 (INSERT INTO outbox ...) -> 여기까지 원자성 보장
  2. 비동기 스케줄러:
    • SELECT * FROM outbox WHERE processed = false 조회
    • Kafka로 메시지 발행
    • 성공 시 UPDATE outbox SET processed = true (또는 삭제)

코드 예시 (Spring Scheduler)

Java
 
// 1. 비즈니스 로직 (회원 가입)
@Transactional
public void join(Member member) {
    memberRepository.save(member);
    
    // 이벤트를 바로 Kafka로 안 보내고, DB에 저장함
    OutboxEvent event = new OutboxEvent("MemberCreated", member.getId());
    outboxRepository.save(event);
}

// 2. 폴링 스케줄러 (별도 스레드)
@Scheduled(fixedDelay = 1000) // 1초마다 실행
public void publishEvents() {
    List<OutboxEvent> events = outboxRepository.findAllByProcessedFalse();
    
    for (OutboxEvent event : events) {
        try {
            // 실제 메시지 발행
            kafkaProducer.send(event.getTopic(), event.getPayload());
            
            // 발행 성공 처리
            event.setProcessed(true);
            outboxRepository.save(event);
        } catch (Exception e) {
            // 실패 시 재시도 로직
        }
    }
}

장단점

  • 장점: 별도의 인프라 없이 DB와 애플리케이션 코드만으로 구현 가능합니다. 구현 난이도가 낮습니다.
  • 단점:
    • DB 부하: 스케줄러가 계속 DB를 찌르기(Polling) 때문에 데이터가 많아지면 DB 성능에 영향을 줍니다.
    • 지연(Latency): 스케줄링 주기(예: 1초)만큼 메시지 발행이 지연됩니다. 실시간성이 떨어질 수 있습니다.

2. 방식 2: 트랜잭션 로그 테일링 (Log Tailing / CDC) - "고성능 실시간"

DB의 **트랜잭션 로그(Binary Log, WAL)**를 실시간으로 감지하여 메시지를 발행하는 방식입니다. 이를 **CDC(Change Data Capture)**라고 부르며, Debezium 같은 도구를 사용합니다.

동작 순서

  1. 트랜잭션 범위 내:
    • 비즈니스 데이터 저장 + outbox 테이블에 이벤트 저장. (애플리케이션 할 일 끝)
  2. CDC 커넥터 (Debezium):
    • DB의 로그 파일(Binlog)을 감시하다가 outbox 테이블에 INSERT가 발생하면 즉시 낚아챕니다.
    • 낚아챈 데이터를 Kafka로 전송합니다.

구조 개념 (애플리케이션 코드가 아님)

Plaintext
 
[Spring Boot App] 
     ↓ (INSERT)
[MySQL Database] -> (Binlog 기록됨)
                        ↓ (감지)
                 [Debezium Connector]
                        ↓ (Publish)
                 [Kafka Topic]

장단점

  • 장점:
    • 리얼타임: 쿼리가 커밋되자마자 거의 실시간으로 이벤트가 발행됩니다.
    • DB 부하 없음: SELECT 쿼리를 날리지 않고 로그 파일을 읽기 때문에 DB 성능에 영향을 주지 않습니다.
  • 단점:
    • 인프라 복잡도: Kafka Connect, Debezium 같은 별도의 미들웨어를 구축하고 운영해야 합니다. 운영 난이도가 급상승합니다.

3. 실무 비교: 언제 무엇을 쓰는가?

구분 Polling Publisher (스케줄러) Log Tailing (CDC/Debezium)
작동 방식 주기적 SELECT 조회 DB 트랜잭션 로그(Binlog) 감지
실시간성 낮음 (스케줄링 주기에 의존) 높음 (거의 즉시)
DB 부하 있음 (지속적인 쿼리 발생) 없음 (로그 파일 읽기)
구축 비용 낮음 (코드 구현) 높음 (별도 인프라 필요)
추천 상황 트래픽이 적거나, 약간의 지연(1~5초)이 허용될 때 대용량 트래픽, 실시간 데이터 동기화가 필수일 때

결론

대부분의 중소규모 서비스에서는 폴링(Polling) 방식으로도 충분합니다. 1~2초 정도 메일이 늦게 간다고 큰일 나지 않으니까요.

하지만 금융 거래, 실시간 재고 동기화처럼 0.1초의 지연도 허용하지 않는 대규모 시스템이라면, 인프라 비용을 감수하고서라도 CDC(Log Tailing) 방식을 도입하여 아키텍처를 고도화해야 합니다.