본문 바로가기
Spring

스프링 @Retry를 이용한 재시도 처리하기

by hseong 2024. 6. 12.

Spring Retry

Spring Retry 프로젝트는 스프링 애플리케이션에 대해 명령형 또는 선언적 재시도 처리를 지원합니다.

의존성

이를 사용하기 위해서는 다음의 의존성을 추가해주어야 합니다. Spring Retry는 스프링 AOP를 이용하여 선언적 방식을 제공합니다. 따라서 AOP 의존성이 필요하며 다른 스타터 의존성에 AOP 의존성도 포함되어있다면 생략해도 됩니다.

implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.retry:spring-retry'

Retryable

사용 방식은 간단합니다. 별도의 configuration에 @EnableRetry를 추가한 뒤, 재시도 처리를 하고 싶은 메서드에 @Retryable만 추가하면 됩니다.

@EnableRetry  // <--
@Configuration  
public class Config {
}

@Slf4j  
@Component  
public class RetryEventListener {  

    private int count = 1;  

    @Retryable  // <--기본값은 3회 호출 시도(최초 호출 포함)
    @Async    
    @EventListener    
    public void test(TestEvent event) {  
        log.info("{}번째 호출입니다.", count++);  
        throw new RuntimeException();  
    }  
}

상세한 설정을 위한 @Retryable의 주요한 속성은 다음과 같습니다.

  • retryFor
    재시도를 수행할 예외 유형을 지정합니다.
    기본값은 모든 예외에 대해 재시도를 수행합니다.
  • noRetryFor
    재시도를 수행하지 않을 예외 유형을 지정합니다.
  • recover
    모든 재시도가 실패했을 경우 호출할 메서드를 지정합니다.
    호출할 메서드에는 @Recover가 붙어있어야 합니다.
  • maxAttempts
    최초 호출을 포함하여 재시도 횟수를 지정합니다.
    기본값은 3회입니다.
  • backoff
    재시도 간격을 조정하기 위한 속성입니다.
    • delay
      밀리초 단위의 재시도 간격을 지정합니다.
    • multiplier
      다음 간격을 계산하기 위해 사용됩니다.
      실패할 떄마다 multiplier^실패 횟수만큼 delay에 곱합니다.
    • maxDelay
      최대 재시도 간격입니다.

이러한 속성을 이용하여 호출 간격과 recovery method를 지정한 것이 다음의 코드입니다.

@Slf4j  
@Component  
public class RetryEventListener {  

    public int count = 1;  

    @Retryable(  
        retryFor = HttpApiException.class,  
        recover = "recoverA",  
        maxAttempts = 3,  
        backoff = @Backoff(delay = 500, multiplier = 2.0))  
    @Async  
    @EventListener  
    public void test(TestEvent event) {  
        log.info("{}번째 호출입니다.", count++);  
        log.info("현재 시간은 {}입니다.", LocalDateTime.now());  
        throw new HttpApiException();  
    }

    @Recover  
    public void recoverA(HttpApiException e, TestEvent event) {  
        log.warn("모든 호출이 실패하였습니다.");  
    }  

    @Recover  
    public void recoverB(HttpApiException e, TestEvent event) {  
        log.error("이건 호출하지 않음");  
    }  
}

HttpApiException이라는 커스텀 예외에 대해 0.5초, 1초 간격으로 3회 시도하고 모두 실패한 경우 recoverA()를 호출하고 종료하도록 설정한 코드입니다.

외부 설정 파일에서 재시도 간격 관리하기

@RetryablemaxAttemptsExpression이나 delayExpression과 같은 속성을 제공합니다. 이를 이용하여 properties 파일에서 재시도 횟수, 간격같은 설정을 별도로 관리하고 표현식을 이용하여 값을 주입할 수 있습니다.

# application.yml
retry:  
  delay: 500  
  multiply: 2.0  
  max-attempt: 3
@Slf4j  
@Component  
public class RetryEventListener {  
    ...
    @Retryable(  
        retryFor = HttpApiException.class,  
        recover = "recoverA",  
        maxAttemptsExpression = "${retry.max-attempt}",  
        backoff = @Backoff(  
            delayExpression = "${retry.delay}",  
            multiplierExpression = "${retry.multiply}"))  
    @Async  
    @EventListener    
    public void test(TestEvent event) {  
        log.info("{}번째 호출입니다.", count++);  
        log.info("현재 시간은 {}입니다.", LocalDateTime.now());  
        throw new HttpApiException();  
    }  
    ...
}

사용 예시

저의 경우 Retry를 적용하게 된 계기가 비동기 실행을 적용한 로직에서 HTTP 통신 중 문제가 발생하는 경우를 고려해야했습니다. 해당 기능이 다른 로직에서 발행한 이벤트를 소비하면서 실행되는 로직이었기 때문에 사용자는 직접적으로 이용할 수 없없습니다. 따라서 재시도를 통해 최소한의 복구 시도가 필요하였습니다.

다음의 코드는 토이 프로젝트를 수행하며 실제 Spring Retry를 적용한 로직입니다.

@Retryable(  
    retryFor = ApiException.class,  
    maxAttemptsExpression = "${retry.max-attempt}",  
    backoff = @Backoff(delayExpression = "${retry.delay}",  
        multiplierExpression = "${retry.multiply}"))  
@Async(value = "generativeTaskExecutor")  
@TransactionalEventListener  
public void createHubTags(CreateHubLinkEvent event) {  
    AutoCreateHubTagCommand command = new AutoCreateHubTagCommand(event.hubId());  
    try {  
        CreateTagResponse response = tagUseCase.autoCreateHubTags(command);  
        log.debug("[Event] 허브 태그 자동 생성. tagIds={}", response.tagIds());  
    } catch (NotMetCondition e) {  
        log.debug("[Event] 허브 태그 자동 생성 취소. 조건을 만족하지 않음.");  
    }
}  

@Recover  
public void recover(RuntimeException e) {  
    log.error("[Event] 태그 자동 생성 실패.", e);  
    throw e;  
}

참고

https://github.com/spring-projects/spring-retry