공부/Spring

재시도를 위한 Spring Retry @Retryable

d02 2024. 4. 27. 20:14

Spring Retry

DB, 네트워크 문제, 동시성 등 일시적인 수행 실패로 인해 재수행 처리를 해야 하는 경우가 있다.
이 때 try catch문과 반복문을 사용해서 직접 재시도 로직을 구현할 수도 있겠지만, 비즈니스 로직과의 관심사 분리가 어렵게 될 것이다.

그런 문제는 AOP로 구현하면 해결되겠지만.. 귀찮을 것이다

이러한 재처리 로직을 AOP를 통해 쉽게 적용할 수 있는 Spring 라이브러리가 있다는 것을 최근 업무 중 알게 되었다.

Spring Retry 라이브러리는 Spring 어플리케이션에서의 재시도 기능을 제공한다.

 

GitHub - spring-projects/spring-retry

Contribute to spring-projects/spring-retry development by creating an account on GitHub.

github.com

 

설정

아래와 같이 의존성을 추가해준다. 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에 대한 익명 함수로 전달할 수 있다.
  • RetryTemplateRetryOperations의 구현체이다.

 

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으로 적용됨을 알 수 있다.

반응형