Outbox와 DLQ 아키텍처 설계
2025. 12. 1. 23:47

안녕하세요! 오늘은 마이크로서비스 환경에서 메시징 신뢰성을 보장하는 두 가지 핵심 패턴인 DLQ(Dead Letter Queue)Outbox 패턴에 대해 알아보고, 실제 프로젝트에 어떻게 적용했는지 공유하려고 합니다.

문제 인식: "메시지가 사라지면 어떡하지?"

저희 팀은 KimLeePark-Logistics라는 물류 시스템을 개발하면서 이벤트 기반 마이크로서비스 아키텍처를 구축하고 있었습니다. Order, User, Payment, Delivery 등 여러 서비스가 Kafka를 통해 통신하는 구조였죠.

초기 설계를 하면서 두 가지 큰 고민이 생겼습니다:

  1. "주문을 DB에 저장했는데, Kafka로 이벤트를 보내기 전에 서버가 죽으면?"
  2. "Consumer가 메시지를 받았는데 처리에 실패하면?"

이 두 문제를 해결하기 위해 도입한 것이 바로 Outbox 패턴과 DLQ입니다.

DLQ vs Outbox: 뭐가 다른 거야?

처음에는 "둘 다 메시지 처리랑 관련된 거 아냐?"라고 생각했는데, 완전히 다른 목적을 가진 패턴이었습니다.

DLQ (Dead Letter Queue)

목적: 메시지 소비 실패 처리
위치: Consumer 측
언제 사용: Consumer가 메시지를 받았지만 처리에 실패했을 때

@KafkaListener(topics = "order-created")
public void consumeOrderEvent(OrderEvent event) {
    try {
        // 결제 처리
        processPayment(event);
    } catch (Exception e) {
        log.error("Payment processing failed: {}", e.getMessage());
        // 재시도 후에도 실패하면 DLQ로 전송
        kafkaTemplate.send("order-created.DLQ", event);
    }
}

Outbox 패턴

목적: 메시지 발행 신뢰성 보장
위치: Producer 측
언제 사용: DB 트랜잭션과 메시지 발행의 원자성을 보장할 때

@Transactional
public void createOrder(OrderRequest request) {
    // 1. 주문 저장
    Order order = orderRepository.save(new Order(request));
    
    // 2. 같은 트랜잭션 내에서 Outbox에 이벤트 저장
    OutboxEvent event = OutboxEvent.builder()
        .aggregateType("Order")
        .aggregateId(order.getId())
        .eventType("OrderCreated")
        .payload(objectMapper.writeValueAsString(order))
        .published(false)
        .build();
    
    outboxRepository.save(event);
    // 트랜잭션 커밋 성공 = 이벤트 발행 보장!
}

// 별도 스케줄러가 Outbox를 폴링하여 Kafka로 발행
@Scheduled(fixedDelay = 1000)
public void publishEvents() {
    List<OutboxEvent> unpublished = outboxRepository
        .findByPublishedFalse();
    
    unpublished.forEach(event -> {
        kafkaTemplate.send(event.getEventType(), event.getPayload());
        event.setPublished(true);
        outboxRepository.save(event);
    });
}

핵심 차이점 정리

구분 Outbox DLQ

목적 메시지 발행 보장 메시지 소비 실패 처리
위치 Producer (생산자) Consumer (소비자)
문제 영역 DB 트랜잭션과 메시지 발행의 일관성 비즈니스 로직 처리 실패
저장소 각 서비스의 DB 테이블 Kafka 토픽

실전 적용: 이벤트 흐름도 설계 과정

이제 실제로 저희 팀이 어떻게 이 패턴들을 적용했는지 보여드리겠습니다.

1단계: 초기 설계 - 기본 이벤트 흐름

처음에는 서비스 간 직접 통신과 이벤트 발행이 혼재된 상태였습니다. 각 서비스가 어떤 이벤트를 발행하고 구독하는지는 정리되어 있었지만, Outbox에 대한 설계는 없었습니다.

2단계: Outbox 추가

그러나 프로젝트를 진행하면서 이 구조로 이어지지 않고 또 새로운 기능들이 추가 되며 구조가 바꼈습니다.

3단계: 여러 서비스 추가

전에 구조와 다르게 이제 동기와 비동기 통신이 추가적으로 생겨서 어느 부분에서는 동기 통신이 다른 부분에서는 비동기 통신이 필요하게 되었습니다.

4단계: 완성된 아키텍처

드디어 완성된 아키텍처입니다!

1.  Outbox와 DLQ를 고려하여 설계 하였습니다.

2. Outbox에 기록 후 kafka를 접근해 메세지를 전달하고
3. 메세지를 소비하다가 오류가 나면 DLQ에 저장될 수 있도록 설계하였습니다.

마치며

DLQ와 Outbox 패턴은 각각 다른 문제를 해결하지만, 함께 사용하면 메시징 시스템의 신뢰성을 크게 높일 수 있습니다.

  • Outbox: "메시지를 반드시 보낸다"
  • DLQ: "실패한 메시지를 놓치지 않는다"

처음에는 복잡해 보였지만, 실제로 구현하고 나니 시스템의 안정성이 눈에 띄게 좋아졌습니다. 특히 장애 상황에서 메시지 유실 걱정 없이 복구할 수 있다는 점이 가장 큰 장점이었습니다.

여러분의 마이크로서비스 프로젝트에도 이 패턴들을 적용해보시길 추천합니다!

'👍코드 회고' 카테고리의 다른 글

Saga 패턴2  (0) 2025.11.27
Saga 패턴  (0) 2025.11.25
N+1 (10.17)  (0) 2025.10.17
리팩토링 vs 오버 엔지니어링 (10.16)  (0) 2025.10.16
장바구니 코드 속 store_id는 어디에 둘까? (10.15)  (0) 2025.10.15