Spring Retry
DB, 네트워크 문제, 동시성 등 일시적인 수행 실패로 인해 재수행 처리를 해야 하는 경우가 있다.
이 때 try catch문과 반복문을 사용해서 직접 재시도 로직을 구현할 수도 있겠지만, 비즈니스 로직과의 관심사 분리가 어렵게 될 것이다.
그런 문제는 AOP로 구현하면 해결되겠지만.. 귀찮을 것이다
이러한 재처리 로직을 AOP를 통해 쉽게 적용할 수 있는 Spring 라이브러리가 있다는 것을 최근 업무 중 알게 되었다.
Spring Retry 라이브러리는 Spring 어플리케이션에서의 재시도 기능을 제공한다.
- 선언적으로도, 명령형으로도 설정할 수 있다.
- https://github.com/spring-projects/spring-retry
설정
아래와 같이 의존성을 추가해준다. AOP 의존성도 필요하다.
implementation ("org.springframework.retry:spring-retry")
implementation ("org.springframework:spring-aspects")
@Retryable을 사용해 선언적으로 적용하기
@Configuration
@EnableRetry
public class Application {
}
@Service
class Service {
@Retryable(retryFor = RemoteAccessException.class)
public void service() {
// ... do something
}
@Recover
public void recover(RemoteAccessException e) {
// ... panic
}
}
@EnableRetry
어노테이션을@Configuration
클래스에 적용한다.@Retryable
:service
메서드가RemoteAccessException
에 의해 실패할 경우 재시도한다.maxAttempts
: 최대 시도 횟수, (기본값: 3회)backoff
: 재시도 전 시간 설정delay
: 재시도 전 대기 시간multiplier
: 다음 재시도 시, 현재 대기 시간의 배수maxDelay
: 재시도 간 최대 기다릴 수 있는 시간random
: 대기 시간을 랜덤으로 설정 (boolean)xxxExpression
속성들로는 각 옵션을 SpEL로 표현할 수 있다.
recover
: Recover 메서드가 2개 이상이면, 어떤 것을 사용할지 지정한다.nonRecoverable
: 특정 예외를 recover 대상에서 제외 가능
@Recover
: 재시도가 성공하지 못했다면,recover
메서드 수행을 시도한다.
위와 같이 @Retryable
어노테이션을 적용하여, 런타임에 AOP 클래스들에 의존성을 적용한다.
RetryTemplate을 사용해 명령형으로 적용하기
RetryTemplate template = RetryTemplate.builder()
.maxAttempts(3)
.fixedBackoff(1000)
.retryOn(RemoteAccessException.class)
.build();
template.execute(ctx -> {
// ... do something
});
- Spring Retry에서 제공하는
RetryOperations
인터페이스를 활용할 수 있다. - 이 인터페이스가 정의하는 메서드
execute
에 실행하고자 하는 로직을,RetryCallback
에 대한 익명 함수로 전달할 수 있다. - 마찬가지로 복구 시 수행하고자 하는 로직 또한
RecoveryCallback
에 대한 익명 함수로 전달할 수 있다. RetryTemplate
은RetryOperations
의 구현체이다.
RetryCallback, RecoveryCallback과 RetryContext
public interface RetryCallback<T, E extends Throwable> {
T doWithRetry(RetryContext context) throws E;
}
- 필요에 따라, 재시도를 반복하는 동안 필요한 데이터를
RetryContext
에 저장해둘 수 있다. - context는 기본적으로
ThreadLocal
에 저장되며,RetrySynchronizationManager.getContext()
를 사용해 직접 접근할 수 있다. - RetryContext에서는 retryCount 를 통해 현재 재시도 횟수를 알 수도 있다.
Listener
public interface RetryListener {
default <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
return true;
}
default <T, E extends Throwable> void onSuccess(RetryContext context, RetryCallback<T, E> callback, T result) {
}
default <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback,
Throwable throwable) {
}
default <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback,
Throwable throwable) {
}
}
RetryListenr
인터페이스를 사용해 여러 재시도 처리 로직들에 대한 횡단 관심사를 콜백으로 적용할 수 있다.RetryTemplate
사용 시 리스너를 등록하거나,@Configruation
클래스에서 복수개의 리스너를 빈으로 등록하여 사용할 수 있다.
@Transactional 과 @Retryable의 적용 순서
두 가지 모두 AOP로 동작한다.
그리고 일반적으로 @Retryable 어노테이션은 @Transacitonal과 같이 쓰게 된다.
그렇다면 이 두 프록시의 실행 순서는 어떻게 될까?
각각의 재시도 처리를 독립된 트랜잭션으로 보장하기 위해서는,
@Transactional로 생성되는 AOP 보다 @Retryable AOP가 먼저 실행되어야 할 것이다.
그리고 다른 AOP가 재시도 - 트랜잭션 사이에 끼어들지 않도록, 바로 직전에 실행되는 게 좋을 것이다.
@Order
Spring AOP에서는 @Order
를 통해 Advice가 적용될 순서를 지정한다.
Order의 값은 정수(int)로, 값이 작을 수록 우선 순위가 높다.
- @Tranactional의 Order 기본값:
Ordered.LOWEST_PRECEDENCE
-> 항상 마지막에 적용된다. - @Retryable의 Order 기본값:
Ordered.LOWEST_PRECEDENCE - 1
-> 항상 마지막에서 두번째로 적용된다.
따라서 적용 순서에 대해 다른 요구사항이 없다면 Order에 대해 별도로 설정해줄 것은 없고, 재시도 시에는 새로운 Transaction으로 적용됨을 알 수 있다.
'공부 > Spring' 카테고리의 다른 글
Spring은 어떻게 이벤트를 발행하고 구독하게 할까? (Spring 이벤트 동작 원리) (0) | 2023.11.17 |
---|---|
Jpa 이벤트를 사용해 일관성 있는 시간 정보 관리하기 (0) | 2023.10.15 |
성능 향상을 위한 Hikari Connection Pool 설정 for MySQL (1) (0) | 2023.09.29 |
Spring Boot의 로깅, 로깅을 왜 할까? (0) | 2023.05.23 |
[문제 해결] @AutoConfigureTestDatabase 설정으로 @JdbcTest에서 원하는 DB 사용하기 (0) | 2023.05.07 |