문제 정의
이벤트성 상품 판매 시스템(s-market)에서는 다음 두 가지 문제를 해결해야 합니다.
재고 초과 판매 방지
선착순 이벤트에서는 재고 초과가 치명적입니다. (“10개 한정” 상품이 11개 팔려서는 안 됩니다.)
Kafka 중복 이벤트 수신
Kafka는 at-least-once delivery를 보장하기 때문에, 동일한 메시지가 Consumer에 여러 번 도달할 수 있습니다.
만약 중복 이벤트에 대해 적절한 대응을 하지 않으면 재고를 불필요하게 다시 차감하거나 잘못된 복구 작업으로 재고 데이터가 오염될 수 있습니다.
문제 상황 분석
단순한 소비 로직의 한계
초기에는 Kafka Consumer에서 StockDeductedEvent를 수신하자마자 DB 차감 시도하고, 이미 처리된 orderId라면 Redis 재고 복구하는 방식으로 로직을 구성했습니다.
if (orderDuplicationRepository.isAlreadyProcessed(event.orderId())) {
stockRedisRepository.increase(event.productId(), event.quantity());
return;
}
하지만 이 방식은 치명적인 문제를 가지고 있었습니다.
- 중복 이벤트가 수신될 때마다 Redis 재고를 무조건 증가시킵니다.
- 최초 주문 처리 자체가 실패한 경우(예: 재고 부족)에도 복구가 이루어져, 잘못된 재고 데이터가 Redis에 기록될 수 있습니다.
- 이 잘못된 복구가 누적되면, Redis와 DB 간 데이터 불일치가 발생하고, 재고 시스템의 신뢰도가 무너질 수 있습니다.
개선 방향
정상 처리에 성공한 주문만 처리 완료로 인식하고, 실패한 주문은 별도로 복구 처리해야 합니다.
이를 위해 아래와 같은 전략을 수립했습니다.
정상 처리 흐름
정상 주문만 처리 완료로 인식하고, 중복 이벤트는 무시합니다.
DB 차감 실패 시에는 예외를 던져 Kafka Retry → DLT로 넘깁니다.
@Transactional
@KafkaListener(topics = "stock.deducted", groupId = "product-service")
public void consumeStockDeductedEvent(final StockDeductedEvent event) {
if (orderDuplicationRepository.isAlreadyProcessed(event.orderId())) {
log.info("이미 처리된 주문 orderId: {}", event.orderId());
return;
}
int updatedRows = stockJpaRepository.decreaseStock(event.productId(), event.quantity());
if (updatedRows == 0) {
throw new BusinessException(ErrorCode.STOCK_NOT_ENOUGH);
}
orderDuplicationRepository.saveProcessed(event.orderId());
}
정상적으로 DB 차감에 성공한 경우에만 orderId를 저장합니다. 실패 시에는 예외를 발생시켜 DLT로 이동시킵니다.
실패 복구 흐름
DLT로 이동한 실패 이벤트만 별도의 Dead Letter Consumer에서 수신하여 이때만 Redis 재고를 복구합니다.
@KafkaListener(topics = "stock.deducted.DLT", groupId = "product-service-dlt")
public void consumeFailedStockDeduction(final StockDeductedEvent event) {
log.error("Dead Letter 수신 - 재고 복구 진행. event: {}", event);
stockRedisRepository.increase(event.productId(), event.quantity());
}
실패한 주문만 복구하므로, 정상 주문에 대한 불필요한 복구는 발생하지 않습니다. 재고 데이터의 정합성을 끝까지 유지할 수 있습니다.
해결한 문제
- Kafka의 at-least-once 특성으로 인한 중복 이벤트 문제를 안전하게 해결했습니다.
- 정상 흐름과 실패 복구 흐름을 명확히 분리하여, 데이터 오염 없이 이벤트를 처리할 수 있게 되었습니다.
- 대규모 이벤트 트래픽에서도 빠른 응답성과 재고 정확성을 동시에 확보할 수 있었습니다.
'Project' 카테고리의 다른 글
[s-market] Kafka 중복 메시지 소비로 인한 재고 이중 차감 (0) | 2025.04.30 |
---|---|
[s-market] Kafka 파티션 키 전략으로 이벤트 순서 보장하기 (0) | 2025.04.29 |
[s-market] Redis 이벤트성 상품 재고 차감 전략 (0) | 2025.04.29 |
[s-market] Consumer 재시도 전략 (0) | 2025.04.28 |
[s-market] 상품과 재고처리 시나리오 계획 (0) | 2025.04.28 |