이번 장에서는 Spring Retry에 대하여 실습해 보겠습니다. Spring Retry는 실패한 동작을 자동으로 다시 호출하는 기능을 제공합니다. 이는 일시적인 네트워크 결함과 같이 오류가 일시적 일 수 있는 경우에 유용합니다.

아래 포스팅을 참고하여 SpringBoot Gradle 프로젝트를 생성합니다.

Build.gradle

dependencies에 다음 내용을 추가합니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.retry:spring-retry:1.1.5.RELEASE'
    implementation 'org.springframework:spring-aspects'
}

Enabling Spring Retry

애플리케이션에서 Spring Retry를 활성화하려면 @Configuration 클래스에 @EnableRetry 애너테이션을 추가해야합니다.

@EnableRetry
@SpringBootApplication
public class RetryApplication {
    public static void main(String[] args) {
        SpringApplication.run(RetryApplication.class, args);
    }
}

Retry With Annotations

@Retryable

@Retryable 애너테이션을 사용하여 실패 시 재 시도할 메서드 호출을 작성할 수 있습니다.

@Service
public class MyService {

    static int retryCount = 0;

    @Retryable(
            value = {SQLException.class},
            maxAttempts = 2,
            backoff = @Backoff(delay = 2000))
    int countContents() throws SQLException {
        retryCount++;
        System.out.println("retryCount " + retryCount);
        if (retryCount == 2) {
            return 100;
        } else {
            throw new SQLException();
        }
    }
}

이 예제에서는 메서드가 SQLException을 발생시키는 경우에 재 시도합니다. 최대 2번의 재시도를 2000 millisecond의 텀을 두고 시도합니다. 예제에서는 2번째 시도 시엔 처리가 성공하였다고 가정하고 값을 리턴합니다. 만약 @Retryable을 아무 속성 없이 사용할 경우 예외가 발생하여 메서드가 실패하면 기본적으로 1초의 지연으로 최대 3번 재시도합니다.

@Retryable(
            value = {SQLSyntaxErrorException.class},
            maxAttempts = 2,
            backoff = @Backoff(delay = 2000))
    int insertContents() throws SQLSyntaxErrorException {
        retryCount2++;
        System.out.println("retryCount " + retryCount2);
        throw new SQLSyntaxErrorException();
 }

이 예제는 첫 번째 예제와는 달리 2번 시도 시에도 실패하는 경우를 보여줍니다.

@Recover

@Recover 애너테이션은 @Retryable 메소드가 지정된 예외로 실패 할 때 별도의 복구 메서드를 정의하는 데 사용됩니다. 두번째 예제에서 재시도 후에도 실패하는 경우 후 처리가 가능합니다.

    @Retryable(
            value = {SQLIntegrityConstraintViolationException.class},
            maxAttempts = 4,
            backoff = @Backoff(delay = 2000))
    int deleteContents(String sql) throws SQLIntegrityConstraintViolationException {
        retryCount3++;
        System.out.println("retryCount " + retryCount3);
        throw new SQLIntegrityConstraintViolationException();
    }

    @Recover
    public int recover(SQLIntegrityConstraintViolationException e, String sql) {
        System.out.println("Recover called : message=" + e.getMessage() + ", sql=" + sql);
        return 50;
    }

deleteContents() 메서드에서SQLIntegrityConstraintViolationException을 발생하면 maxAttempt만큼 재시도 후 그래도 복구가 안되었을 경우엔 recover() 메서드가 최종 호출됩니다.

recover 선언 시 주의할 점은 처리하려는 메서드의 매개변수와 리턴 타입을 동일하게 선언해야 한다는 것입니다. 위의 recover 메서드를 보면 Throwable 매개변수 뒤에 deleteContents의 첫 번째 매개 변수가 세팅된 것을 볼 수 있습니다.

RetryTemplate

RetryOperations

Spring Retry는 일련의 execute() 메소드를 제공하는 RetryOperations 인터페이스를 제공합니다.

public interface RetryOperations {
    <T> T execute(RetryCallback<T> retryCallback) throws Exception;
    ...
}

execute()의 매개 변수 인 RetryCallback은 실패 시 재 시도해야 하는 비즈니스 로직 삽입을 허용하는 인터페이스입니다.

public interface RetryCallback<T> {
    T doWithRetry(RetryContext context) throws Throwable;
}

RetryTemplate Configuration

RetryTemplate은 RetryOperations의 구현체입니다. @Configuration 클래스에서 RetryTemplate Bean을 구성 해보겠습니다.

@EnableRetry
@Configuration
public class RetryApplication {
    //...
    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
		
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(2000l);
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
 
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(2);
        retryTemplate.setRetryPolicy(retryPolicy);
		
        return retryTemplate;
    }
}

RetryPolicy는 작업 재시도 시기를 결정합니다. SimpleRetryPolicy는 고정된 횟수만큼 재 시도하는 데 사용됩니다.

BackOffPolicy는 재시도 간의 백 오프를 제어하는 ​​데 사용됩니다. FixedBackOffPolicy는 계속하기 전에 일정 시간 동안 일시 중지합니다.

RetryTemplate의 사용

// callback 방식
retryTemplate.execute(new RetryCallback<Integer, RuntimeException>()  {
            @Override
            public Integer doWithRetry(RetryContext context) {
                return myService.countContentsForRetryTemplate();
            }
});
// lambda 방식
retryTemplate.execute(context -> myService.countContentsForRetryTemplate());

Listeners

리스너는 재 시도에 추가 콜백을 제공합니다. 재 시도에 따른 다양한 문제에 사용할 수 있습니다. 열기 및 닫기 콜백은 전체 재시도 전후에 발생하며 onError는 개별 RetryCallback 호출에 적용됩니다.

public class DefaultListenerSupport extends RetryListenerSupport {
    @Override
    public <T, E extends Throwable> void close(RetryContext context,
      RetryCallback<T, E> callback, Throwable throwable) {
        logger.info("onClose);
        // 로직
        super.close(context, callback, throwable);
    }
 
    @Override
    public <T, E extends Throwable> void onError(RetryContext context,
      RetryCallback<T, E> callback, Throwable throwable) {
        logger.info("onError"); 
        // 로직
        super.onError(context, callback, throwable);
    }
 
    @Override
    public <T, E extends Throwable> boolean open(RetryContext context,
      RetryCallback<T, E> callback) {
        logger.info("onOpen);
        // 로직
        return super.open(context, callback);
    }
}

Registering the Listener

@EnableRetry
@SpringBootApplication
public class RetryApplication {
    public static void main(String[] args) {
        SpringApplication.run(RetryApplication.class, args);
    }

    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();

        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(2000l);
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(2);
        retryTemplate.setRetryPolicy(retryPolicy);
        retryTemplate.registerListener(new DefaultListenerSupport());
        return retryTemplate;
    }
}

여기까지 Spring Retry를 간단히 실습해 보았습니다. 애너테이션과 RetryTemplate을 사용한 재시도 예제를 해보았고, 리스너를 사용하여 추가 콜백도 구성해 보았습니다. 실습한 예제코드는 아래 GitHub에서 확인할 수 있습니다.

https://github.com/codej99/Spring-Retry