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()
를 호출하고 종료하도록 설정한 코드입니다.
외부 설정 파일에서 재시도 간격 관리하기
@Retryable
은 maxAttemptsExpression
이나 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;
}
참고
'Spring' 카테고리의 다른 글
Jedis, Lettuce, Redisson. 레디스를 위한 세 가지 자바 클라이언트 (1) | 2024.10.30 |
---|---|
스프링 @Async를 이용한 비동기 실행 적용하기 (1) | 2024.06.11 |
스프링에서 외부 API 호출을 테스트하는 방법(feat. RestClient) (0) | 2024.05.20 |
스프링 이벤트(Spring Event) 사용해보기 (0) | 2024.02.12 |
로그 세팅하기 (1) | 2023.11.19 |