💡문제 상황
우리 프로젝트에서는 주문을 생성할 때 배송 정보도 함께 입력받는 구조를 채택하고 있다.
현재 주문 처리 흐름은
주문 ➡️ 재고 확인 ➡️ 결제 ➡️ 재고 차감 ➡️ 배송 ➡️ 주문 완료
순으로 진행된다.
여기서 맞닥뜨렸던 고민이 있었다.
"배송 정보는 결제가 완료되어야만 유효한데, 주문 생성 시점에서 이 정보를 어떻게 안전하게 처리할 수 있을까?"
예를 들어, 사용자가 주문을 시작하면서 배송지 정보를 함께 입력했다고 해보자. 그런데 결제를 하지 않고 중간에 이탈해버린다면?
결과적으로 결제가 완료되지 않은 주문과 배송지 정보가 DB에 불필요하게 영구 저장될 수 있다. 사용자 수가 많아질수록, 예컨대 100명이 결제 없이 이탈한다면 그만큼 불필요한 주문 데이터와 함께 민감한 개인정보(주소, 연락처 등)가 DB에 쌓이게 된다. 이는 데이터 정합성과 개인정보 보호 측면 모두에서 문제가 될 수 있다.
따라서, 우리는 결제가 아직 완료되지 않은 상태에서 입력받은 배송 정보를 어디에 저장하고 어떻게 처리할지에 대한 설계 고민이 필요했다.
// OrderCreateRequest 예시
{
"orderDetails": [
{
"productId": "dd3e9605-a713-48bf-b87e-aa93393412f8",
"orderQuantity": 1
},
{
"productId": "f3de0033-7e8e-40a6-9b1d-77e6f3712be8",
"orderQuantity": 1
}
],
"deliveryInfo": {
"deliveryRequirement": "문 앞에 놓아주세요",
"recipientName": "홍길동",
"address": "서울특별시 중구 을지로 100",
"contact": "010-1234-5678"
}
}
💡고려했던 기술 선택지
1. 주문 엔티티에 배송 정보를 추가해 함께 저장하는 방법
- 장점
- 배송 정보가 주문과 함께 영구적으로 DB에 저장되므로, 별도의 저장소를 두지 않아도 관리가 쉽다.
- 단점
- 배송과 주문 모듈 간의 강한 결합이 생긴다.
- 그로 인해 MSA 환경에서 모듈 간 독립성이 깨지고, 도메인 분리나 확장이 어려워진다.
2. 결제 요청과 배송 요청을 동시에 처리하는 방법 (Kafka 활용)
- 장점
- Kafka를 이용한 비동기 처리를 통해, 각 모듈 간 실시간 데이터 전송이 가능해진다.
- 단점
- 결제가 실패해도 배송 요청이 전송될 수 있다. (= 결제 완료 여부와 상관없이 배송 처리가 진행될 위험이 있음)
- Kafka를 사용한 비동기 메시징의 특성상 정확한 순서 보장이나 데이터 일관성 확보가 어렵다.
3. 배송 정보를 Redis에 임시 저장하는 방법 (✅ 선택)
- 장점
- 결제가 완료되기 전까지 배송 데이터를 안전하게 임시 보관할 수 있다.
- Redis의 빠른 속도와 TTL 설정으로 불필요한 데이터를 자동으로 정리할 수 있다.
- 주문/배송 모듈 간 결합도를 낮춰 모듈 구조를 유연하게 설계할 수 있다.
- 단점
- Redis에 장애 발생 시 임시 저장된 데이터의 유실 위험이 존재한다.
- 이런 문제를 방지하기 위해 마스터-슬레이브 구성이나 클러스터링과 같은 고가용성 설계가 필요하다.
💡선택한 전략 : Redis에 배송 정보 임시 저장
이 방식은 다음과 같은 이유로 선택하게 되었다:
- MSA 구조에서 각 도메인의 독립성을 유지할 수 있고, 주문 및 배송 데이터의 결합을 피하면서 유연한 시스템 구성이 가능하다.
- 결제 완료 이후에만 배송 처리를 시작함으로써 데이터 일관성과 서비스 흐름의 명확한 분리를 달성할 수 있다.
- 마스터-슬레이브 패턴을 구현하면 데이터 손실 문제도 해결할 수 있다.
배송 정보는 주문 생성 시 바로 DB에 영구 저장되지 않는다. 대신, Redis에 orderId를 Key로 하여 임시 저장되고, 결제 완료 후에만 실제 배송 모듈로 전달된다.
✏️구현 과정
1. 주문 생성 시 배송 정보 임시 저장
- 주문 생성 API 호출 시 배송 정보를 Redis에 저장한다.
- Key는 orderId 로 하고, TTL (Time to Live) 을 설정하여 5분이 지나면 자동 삭제되도록 구성한다.
- 즉, 임시로 저장한 배송 정보도 함께 삭제되므로 DB 정합성과 보안 모두 유지할 수 있다.
// 배송정보 Redis 저장
String redisKey = orderId.toString();
String deliveryInfoJson = EventSerializer.serialize(deliveryInfo);
redisTemplate.opsForValue()
.set(redisKey, deliveryInfoJson, 300, TimeUnit.SECONDS); // 5분 TTL 설정
2. 결제 완료 시 배송 처리
- 결제가 성공하면 Kafka를 통해 payment-create-response 이벤트를 수신한다.
- Redis에서 해당 orderId의 배송 정보를 조회한 후, 배송 모듈로 전달하고 Redis에서는 데이터를 삭제한다.
2-1. Kafka Consumer
// OrderMessagingConsumerService.java
@KafkaListener(topics = "payment-create-response", groupId = "commerce-service")
public void listenPaymentCreateResponse(String message) {
PaymentCreateResponseEvent event = EventSerializer.deserialize(message, PaymentCreateResponseEvent.class);
UUID orderId = event.getOrderId();
UUID paymentId = event.getPaymentId();
orderEventService.handlePaymentComplete(orderId, paymentId);
}
2-2. 결제 완료 이벤트 처리
// OrderEventService.java
@RequiredArgsConstructor
@Service
public class OrderEventService {
private final OrderMessagingProducerService orderMessagingProducerService;
private final KafkaTemplate<String, Object> kafkaTemplate;
/**
* payment-create-response
*/
@Transactional
public void handlePaymentComplete(UUID orderId, UUID paymentId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new CustomException(CommerceErrorCode.ORDER_NOT_FOUND));
// 재고 차감
decreaseStockForOrder(order);
// 결제 ID 지정
order.assignPaymentId(paymentId);
// 주문 상태 저장
orderRepository.save(order);
// 배송 생성 요청
messagingProducerService.sendDeliveryCreateRequest(orderId, order.getUsername());
}
}
2-3. Kafka Producer (배송 요청)
// OrderMessagingProducerService.java
@RequiredArgsConstructor
@Service
public class OrderMessagingProducerService {
private final KafkaTemplate<String, String> kafkaTemplate;
private final TemporaryStorageService temporaryStorageService;
/**
* Kafka로 배송 생성 요청을 전송하는 메서드
*/
public void sendDeliveryCreateRequest(UUID orderId, String username) {
// Redis에 저장된 배송 정보 조회
OrderCreateRequestDto.DeliveryInfo deliveryInfo = temporaryStorageService.getDeliveryInfo(orderId);
// 배송 생성 이벤트 객체 생성
DeliveryCreateRequestEvent event = new DeliveryCreateRequestEvent(
orderId,
deliveryInfo.getDeliveryRequirement(),
deliveryInfo.getRecipientName(),
deliveryInfo.getAddress(),
deliveryInfo.getContact(),
username
);
// delivery-create-request 메시지 전송
sendMessage(deliveryCreateRequestTopic, event);
// Redis에 저장된 배송 정보 삭제
temporaryStorageService.removeDeliveryInfo(orderId);
}
}
이 구조를 통해 주문 생성 시 임시 저장한 배송 정보를 결제 성공 시에만 배송 처리에 활용할 수 있고, Redis TTL 설정과 결합해 보안성과 데이터 정합성도 확보할 수 있다.
3. 데이터 소실 방지 (Redis 고가용성 구성)
- 비즈니스상 중요한 정보이기 때문에, Redis에 저장된 데이터도 복제 및 장애 복구 전략을 갖춰야 한다.
- Redis 장애에 대비하여 Master-Slave 패턴 또는 Redis Clustering을 활용한다.
✏️실제 서비스 적용 시 고려사항
1. 적절한 TTL 시간 설정
- TTL을 너무 짧게 설정하면 사용자가 결제를 완료하기 전에 배송 정보가 삭제될 수 있다.
- 반대로 너무 길면 메모리를 불필요하게 점유하므로, 평균 결제 완료 시간 기준으로 적절히 조절해야 한다.
2. Redis 장애 시 복구 전략
- 위에서 언급했다시피, Redis는 메모리 기반 저장소이므로 장애 시 데이터가 유실될 수 있다.
- Redis Sentinel이나 Cluster를 통해 복제 및 자동 Failover 환경을 구성해 데이터 손실을 최소화해야 한다.
3. 데이터 무결성
- 결제 완료 이벤트와 배송 요청 로직 간 순서가 어긋나면 문제를 일으킬 수 있으므로, 이벤트 순서 제어 및 재시도 로직이 필요할 수 있다.
- Kafka 소비자에서 처리 실패 시 재시도를 위한 DLT(Dead Letter Topic) 등을 고려할 수 있다.
✔️마무리
이번 포스팅에서는 주문 생성 시 함께 들어오는 배송 정보를 어떻게 안전하고 유연하게 처리할 수 있을지를 고민한 과정을 공유했다.
핵심을 요약해보면 아래와 같다.
" 배송 정보는 결제가 완료되어야 유효하며,
결제 전에 이를 DB에 저장하면 정합성 문제와 불필요한 데이터가 발생할 수 있으므로,
TTL이 설정된 Redis에 임시 저장 후, 일정 시간 내 결제가 완료되지 않으면 자동 취소 처리한다. "
이 방법은 모듈 간 결합도를 낮추면서도, 비즈니스 흐름을 안전하게 분리할 수 있는 전략이 되었다. 이후엔 Redis 마스터-슬레이브 구조를 구축하고 Sentinel을 함께 적용했는데, 이 방법까지 포스팅할 예정이다.
땡큐포와칭!
다음 글
[프로젝트/구현] Redis Replication 마스터-슬레이브 구조를 통한 분산 처리 적용 과정
[프로젝트/구현] Redis Replication 마스터-슬레이브 구조를 통한 분산 처리 적용 과정
이전 글2025.05.01 - [Project/대용량 트래픽 프로젝트] - [기술적 의사결정] MSA 환경에서 배송 정보 임시 저장소로 Redis를 사용한 이유 [기술적 의사결정] MSA 환경에서 배송 정보 임시 저장소로 Redis를
developer-jinnie.tistory.com
'Project > 대용량 트래픽 프로젝트' 카테고리의 다른 글
[프로젝트/구현] Redis Replication 마스터-슬레이브 구조를 통한 분산 처리 적용 과정 (0) | 2025.05.04 |
---|---|
[프로젝트/구현] Redis 분산락으로 재고 감소 동시성 이슈 해결하기 (2/2) (feat. Facade 패턴) (7) | 2025.04.24 |
[프로젝트] Windows 환경에서 JMeter 설치 및 부하 테스트 하기 (8) | 2024.12.11 |
[프로젝트] QueryDSL 사용 시 페이징 응답 JSON 데이터 최적화 하기 (1) | 2024.12.11 |
[프로젝트/기술적 의사결정] Redis 분산락으로 재고 감소 동시성 이슈 해결하기 (1/2) (1) | 2024.12.11 |