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