[retry] spring-retry for auto retry
spring-retry?
exception이 난 작업에 대해 자동으로 재시도 해주는 스프링 라이브러리이다.
일시적인 connection error 등에 유용하게 사용할 수 있다.
환경: springboot2.6.2
implementation 'org.springframework.retry:spring-retry'
다른 출처에서는 아래 의존성도 필요하다고 하는데, 2.6.2 에서는 없어도 돌아간다.
implementation 'org.springframework:spring-aspects'
확인해보니 이미 관련 라이브러리들이 불러와져 있었다.
사용법은 쉽다. @Configuration 쪽 클래스에 아래 어노테이션을 추가하면 사용가능하다.
@EnableRetry
관련 어노테이션을 각 함수 위에 달아두면 그 함수가 설정한 조건에 맞게 실행된다.
1. @Retryable
@Retryable(value = {NoSuchElementException.class}, //언제 실행되는가
maxAttempts = 5, //최대 몇 번 재시도 하는가
backoff = @Backoff(delay = 1000)) //재시도 전 딜레이 시간(ms)
void retryServiceWithRecovery(String value) {
}
NoSuchElementException이 났을 경우 최대 5번 실행하고 각 실행 전 1초씩 쉬도록 설정하였다.
5번이 넘어가도록 실패한다면..? 아래 @Recovery 어노테이션으로 fallback을 구현할 수 있다.
2. @Recovery
@Recover
void recover(NoSuchElementException e, String value){
}
여기서 특이점은, input으로 @Retryable 함수의 input 변수와 Retryable의 exception을 받도록 구현해야 한다는 점.
그리고 output은 @Retryable 함수의 return type과 동일해야한다.
3. 개별 설정 말고 글로벌 설정
1) properties에 설정한 후 클래스로 불러와서 세팅하기(스프링 기본설정 아님)
properties 파일에 대략 아래와 같이 설정하고(변수 이름 아무거나 가능)
retry.maxAttempts=2
retry.maxDelay=100
어노테이션을 아래와 같이 수정한다.
@Retryable( value = SQLException.class,
maxAttemptsExpression = "${retry.maxAttempts}",
backoff = @Backoff(delayExpression = "${retry.maxDelay}"))
void retryServiceWithExternalizedConfiguration(String sql) throws SQLException{
}
이 때 주의해야 하는 것은 maxAttempts -> maxAttemptsExpression, delay -> delayExpression 로 항목명이 바뀌었다는 점!
2) RetryTemplate 사용
전반적으로 사용하는 설정을 RetryTemplate으로 만들어 빈으로 등록해놓고 필요한 곳에서 불러다가 함수 내부에서 호출한다.
@Configuration
@EnableRetry
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(2000l); //long type; 딜레이 시간(ms)
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(2); //횟수
retryTemplate.setRetryPolicy(retryPolicy);
return retryTemplate;
}
}
- bean 적용(before)
@Retryable(value = {NoSuchElementException.class}, maxAttempts = 5, backoff = @Backoff(delay = 1000))
protected void on(AccountCreationEvent event, @Timestamp Instant instant){
log.debug(">>> projecting {} , timestamp : {}", event, instant.toString());
HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
holderAccount.setAccountCnt(holderAccount.getAccountCnt() + 1);
repository.save(holderAccount);
}
- retryTemplate 적용(after)
@RequiredArgsConstructor
...
private final RetryTemplate retryTemplate;
...
protected void on(AccountCreationEvent event, @Timestamp Instant instant){
log.debug(">>> projecting {} , timestamp : {}", event, instant.toString());
retryTemplate.execute(context -> {
log.info("retry going on");
HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
holderAccount.setAccountCnt(holderAccount.getAccountCnt() + 1);
repository.save(holderAccount);
return null;
});
}
참고로 retryTemplate 의 execute 함수는 다음과 같이 4종류가 있다. 상황에 맞게 구현하면 된다. 아래는 관련 용어 설명이다.
- retryCallback은 exception 시 재시도 할 로직
- retryContext는 재시도 시 이전 시도 작업물(실행 간 공유해야 할 데이터가 있을 경우) 들고 실행
- recoveryCalback은 재시도 횟수만큼 전부 실패할 경우 호출되는 로직
- retryState 는 retryContext에 든 상태를 가지고 재시도를 하면 stateful; 아니면 stateless라고 한다. 일반적으로는 stateless가 많고 주로 개별적으로 설정한다.
//retryCallback 만 사용; stateless
@Override
public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E {
return doExecute(retryCallback, null, null);
}
//retryCallback, reoveryCallback 사용; stateless
@Override
public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback,
RecoveryCallback<T> recoveryCallback) throws E {
return doExecute(retryCallback, recoveryCallback, null);
}
//retryCallback, statefulRetry
@Override
public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RetryState retryState)
throws E, ExhaustedRetryException {
return doExecute(retryCallback, null, retryState);
}
//retryCallback, reoveryCallback 사용하는 statefulRetry
@Override
public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback,
RecoveryCallback<T> recoveryCallback, RetryState retryState) throws E, ExhaustedRetryException {
return doExecute(retryCallback, recoveryCallback, retryState);
}
어노테이션과 retryTemplate을 사용하는 것에 각각 장단점이 있는데,
어노테이션은 간편하지만 매번 설정해야하는 불편함이 있고(반면 각각 설정에 대해 가시적으로 알 수 있어 좋을 수도 있다)
retryTemplate은 di 주입방식이라 테스트코드 짜기가 간편, 세부 설정이나 콜백 로직을 작성할 수 있지만 소스가 길어진다는 단점이 있다.
뭐든 캐바캐, 상황에 맞게 사용하면 될 듯 하다.
2-1) 재시도 전/후 콜백 Listener
재시도를 할 때마다 호출되는 콜백 로직을 추가할 수 있다(로그 등). RetryListener를 구현한 구현체를 만든 후 retryTemplate에 등록하면 된다.
//RetryListener.class
//모든 재시도 전
<T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback);
//모든 재시도 후
<T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
//개별 retryCallback 에러 시
<T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
@Slf4j
public class RetryListener extends RetryListenerSupport {
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
log.info(">>>> before retry");
return super.open(context, callback);
}
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
log.info(">>>> after retry");
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
log.info(">>>> error on retry");
}
}
@Configuration
@EnableRetry
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(2000l); //long type; 딜레이 시간(ms)
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(2); //횟수
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.registerListener(new RetryListener()); //listener
return retryTemplate;
}
}
+ 근데 자동으로 재시도하는게 항상 이점만 있을까?
예를 들어 '타 컴포넌트로 정보 조회 -> 결과를 가지고 나의 데이터 수정 작업 -> 타 컴포넌트 데이터 수정' 이러한 로직이 있다고 가정해보자.
이 경우 정보 조회 시 네트워크 에러가 나 전체 작업이 중지되는 경우 이와 같은 재시도가 의미 있을 수 있다. 허나 타 컴포넌트로 저장 작업이 실패해서 계속 재시도를 할 경우.. 제대로된 timeout 설정 등 뭔가가 맞지 않는다면 중복으로 데이터가 들어가지 않거나 다른 장애를 가져올 수 있기 때문에 MSA일 경우 더더욱 심사숙고해야 할 듯 하다.
아 그리고 비슷한 라이브러리로 Resilience4j 가 있다고 하는데, 참고하길 바람!!
다음 시간에는 spring-retry 관련, 간단한 테스트 코드를 짜본다.
2022.02.07 - [개발/spring] - [retry] spring-retry test code
[retry] spring-retry test code
이전글: 2022.02.04 - [개발/spring] - [retry] spring-retry for auto retry [retry] spring-retry for auto retry spring-retry? exception이 난 작업에 대해 자동으로 재시도 해주는 스프링 라이브러리이다. 일..
bangpurin.tistory.com
참고
원문: https://docs.spring.io/spring-batch/docs/current/reference/html/retry.html
Retry
Sometimes, there is some business processing that you know you want to retry every time it happens. The classic example of this is the remote service call. Spring Batch provides an AOP interceptor that wraps a method call in a RetryOperations implementatio
docs.spring.io
재시도에 대한 고찰: https://brunch.co.kr/@springboot/580
Spring Retry
Resilience4j, Spring Retry 재시도 패턴 구현 | Overview 이 글에서는, Resilience4j 및 Spring Retry 라이브러리를 사용해서 "재시도 패턴"을 구현한다. 목차 1. Resilience4j 를 사용해서 구현2. Spring Retry 를 사용해
brunch.co.kr