이번 장에서는 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















