안녕하세요! 오늘은 마이크로서비스 환경에서 메시징 신뢰성을 보장하는 두 가지 핵심 패턴인 DLQ(Dead Letter Queue)와 Outbox 패턴에 대해 알아보고, 실제 프로젝트에 어떻게 적용했는지 공유하려고 합니다.
문제 인식: "메시지가 사라지면 어떡하지?"
저희 팀은 KimLeePark-Logistics라는 물류 시스템을 개발하면서 이벤트 기반 마이크로서비스 아키텍처를 구축하고 있었습니다. Order, User, Payment, Delivery 등 여러 서비스가 Kafka를 통해 통신하는 구조였죠.
초기 설계를 하면서 두 가지 큰 고민이 생겼습니다:
- "주문을 DB에 저장했는데, Kafka로 이벤트를 보내기 전에 서버가 죽으면?"
- "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 |