개발노트

25.03.21 사가 패턴 무한 재시도 방지 본문

TroubleShooting

25.03.21 사가 패턴 무한 재시도 방지

ddong-kka 2025. 3. 21. 23:53

개요

주문이 생성되면 배송 생성 메시지를 전달하고, 배송이 정상적으로 생성되었는지 확인하는 메시지가 온다.
배송이 정상적으로 생성되었을 경우, 주문의 상태를 "배송 대기"로 변경하고, 배송 생성이 실패한 경우에는 배송 생성 요청을 다시 보내게 구현했다.


문제점

현재 시스템에서는 배송의 운행 담당자가 비어있는 상황에서 배송 생성이 계속해서 실패하고 있습니다. 이 경우, 배송 생성 요청이 무한히 재시도되는 문제가 발생한다.

 

/**
 * 배송 생성 실패 시 재시도 요청을 한다.
 *
 * @param shippingResponseMessage 배송 생성 응답 메시지
 */
public void retryCreateShipping(ShippingResponseMessage shippingResponseMessage) {
    
    Order targetOrder = findOrderById(shippingResponseMessage.getOrderId());
    Optional<String> recipientsAddress = findRecipientAddress(targetOrder.getRecipientsId());

    StockDecrementMessage stockDecrementMessage = StockDecrementMessage.builder()
       .supplierId(targetOrder.getSupplierId())
       .build();

    processShippingRequest(targetOrder, stockDecrementMessage, recipientsAddress);
}

 


해결 방법

문제를 해결하기 위해, Redis를 활용하여 재시도 횟수를 추적하고, 재시도가 3회 이상 발생했을 경우 더 이상 재시도를 하지 않고 상태를 변경하도록 구현한다.

 

  1. Redis를 사용하여 주문의 orderId와 관련된 재시도 횟수를 관리한다.
  2. 첫 번째 배송 생성 실패 시, Redis에 재시도 횟수를 기록하고, 이후에는 해당 횟수를 기준으로 3회 이상 실패했을 경우 더 이상 재시도를 멈추게한다.
  3. 배송 생성이 성공하면 Redis의 재시도 횟수를 초기화한다.

해결 과정

 

redis를 사용하기전에 간단한 템플릿 설정을 해준다.

@Configuration
public class RedisTemplateConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
       RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
       redisTemplate.setConnectionFactory(redisConnectionFactory);

       // String 타입은 StringRedisSerializer 사용
       redisTemplate.setKeySerializer(new StringRedisSerializer());
       // Object 타입의 값은 StringRedisSerializer 사용
       redisTemplate.setValueSerializer(new StringRedisSerializer());

       return redisTemplate;
    }
}

 

 

retryCreateShipping

	/**
	 * 배송 생성 실패 시 재시도 요청을 한다.
	 *
	 * @param shippingResponseMessage 배송 생성 응답 메시지
	 */
	public void retryCreateShipping(ShippingResponseMessage shippingResponseMessage) {

		// Redis에서 재시도 카운트를 가져옴
		String orderId = shippingResponseMessage.getOrderId().toString();
		String retryCountKey = RETRY_COUNT_KEY_PREFIX + orderId;

		String retryCountStr = redisTemplate.opsForValue().get(retryCountKey);
		int retryCount = (retryCountStr != null) ? Integer.parseInt(retryCountStr) : 0;

		// 3회 이상 재시도한 경우 상태를 'ORDER_RECEIVED'로 변경하고 종료
		if (retryCount >= 3) {
			updateOrderStatus(findOrderById(shippingResponseMessage.getOrderId()), OrderStatus.ORDER_FAILED);
			return;
		}

		Order targetOrder = findOrderById(shippingResponseMessage.getOrderId());
		Optional<String> recipientsAddress = findRecipientAddress(targetOrder.getRecipientsId());

		StockDecrementMessage stockDecrementMessage = StockDecrementMessage.builder()
			.supplierId(targetOrder.getSupplierId())
			.build();

		processShippingRequest(targetOrder, stockDecrementMessage, recipientsAddress);

		// Redis에 재시도 카운트를 저장
		redisTemplate.opsForValue().set(retryCountKey, String.valueOf(retryCount + 1));
	}

 

 

findOrderById

/**
	 * 주문 ID를 이용해 주문을 조회한다.
	 *
	 * @param orderId 주문 ID
	 * @return 조회된 주문 객체
	 * @throws OrderNotFoundException 주문을 찾을 수 없을 경우 예외 발생
	 */
	private Order findOrderById(UUID orderId) {
		return orderRepository.findByIdAndDeletedAtIsNull(orderId)
			.orElseThrow(() -> new OrderNotFoundException("Order Not Found By Id : " + orderId));
	}

 

 

updateOrderStatus

/**
 * 주문 상태를 업데이트한다.
 *
 * @param targetOrder 대상 주문
 * @param newStatus 새로운 주문 상태
 */
private void updateOrderStatus(Order targetOrder, OrderStatus newStatus) {
    orderRepository.save(targetOrder.toBuilder().status(newStatus).build());
}