Spring - @Retryable 을 이용한 재수행 로직 처리하기
들어가며
서비스를 개발하거나 어떤 로직을 구현할 때 실패에 따른 재수행 로직을 추가하고 싶을 때가 있다. 따로 코드로 구현할 수 있겠지만 스프링을 사용한다면 스프링에서 제공하는 @Retryable 어노테이션을 통해서 간편하게 재수행 로직을 구현할 수 있다.
Spring - @Retryable 을 이용한 재수행 로직 처리하기
@Retryable 사용 방법
먼저 @Retryable을 사용하기 위해서는 다음 두가지 작업을 해줘야 한다.
1. 의존성 추가
implementation("org.springframework.retry:spring-retry")
implementation("org.springframework:spring-aspects")
2. @EnableRetry 어노테이션 추가
다음 두가지 작업이 완료되었다면 이제 실패 시 재수행 로직을 이용하고 싶은 메서드에 @Retryable 어노테이션을 추가해주면 된다.
기본 설정을 알아보기 위해 basicRetry 메서드에 아무 옵션도 추가하지 않은 @Retryable 어노테이션을 추가해주고 정말 로직을 재수행하는지 확인을 하기 위해 count 값을 추가하여 준다.
@Service
class RetryableService {
var count = 0
@Retryable
fun basicRetry(): Int {
return randomFail()
}
private fun randomFail(): Int {
if (random() > 0.1) {
count += 1
println("failed")
throw RuntimeException(count.toString())
} else {
println("success")
return count
}
}
}
@Retryable 어노테이션에 설정되어 있는 기본 재수행 시도 값은 3이다.
이제 테스트 코드를 작성해서 재수행되는 카운트 값을 확인해보고 재수행 기본값으로 설정되어 있는 3번 이하로 실행되는지 확인해본다.
companion object {
const val DEFAULT_RETRY_COUNT = 3
}
@Autowired
lateinit var retryableService: RetryableService
@Test
fun basicRetry() {
try {
val count = retryableService.basicRetry()
println(":: 재시도 횟수 : $count")
Assertions.assertTrue(count <= DEFAULT_RETRY_COUNT)
} catch (e: Exception) {
println(":: 재시도 횟수 : ${e.message} ")
Assertions.assertTrue(e.message?.toInt()!! <= DEFAULT_RETRY_COUNT)
}
}
테스트를 실행해보면 최대 3번까지만 수행하는 것을 확인할 수 있다.
@Retryable 추가 옵션 설정
@Retryable에서 제공하는 여러 옵션들이 있지만 그 중에서 많이 사용하는 추가 옵션들의 대해서 알아본다.
maxAttempts
위에서 알아보았듯이 @Retryable에 기본 재수행 시도 값은 3이다. 근데 경우에 따라서 해당 횟수를 조작하고 싶을 때가 있을 것이다. 이때 maxAttempts 옵션을 사용하면 maxAttempts에 설정한 횟수만큼 재시도하게 된다.
다음과 같이 maxAttempts 옵션을 설정해준다.
@Retryable(maxAttempts = 5)
이제 테스트 코드를 작성하여 확인해보면 다음과 같이 5회까지 재시도하는 것을 확인할 수 있다.
backoff
backoff 옵션을 통해서 로직 재시도 간의 time delay 설정도 가능하다. 설정은 다음과 같이한다. 시간의 단위는 '1000 == 1초' 이다
@Retryable(backoff = Backoff(5000))
다음과 같이 설정하면 5초 간격으로 실행하게 되며 시간을 찍어보면 다음과 같이 5초마다 재수행하는 것을 확인할 수 있다.
include
include 옵션을 통해서 특정 Exception에 대해서만 재시도 로직을 타게 설정할수도 있다. List 형태를 지원하기 때문에 여러 개의 Exception 설정이 가능하다.
추가적으로 exclude 옵션도 있는데 해당 옵션에 선언한 Exception에 대해서는 재시도 로직을 타지 않게 되니 참고하자.
@Retryable(include = [RuntimeException::class, IllegalArgumentException::class, NoSuchElementException::class])
@Recover
마지막으로는 @Recover 어노테이션에 대해 설명한다. @Recover 어노테이션을 이용하면 Retry 수행 후 최종적으로 exception이 발생 시 @Recover를 선언한 특정 메서드를 실행할 수 있게 도와주는 어노테이션이다.
다만 여기서 주의할 점은 exception 인자를 제외한 나머지 입출력이 타겟 메서드와 동일해야 한다.
빠른 이해를 위해 다음 예를 확인 해보자.
다음과 같이 특정 메서드와 @Recover가 붙은 메서드 두개를 추가해주자.
@Retryable(include = [NoSuchElementException::class])
fun retryByNoSuchElementException(input: String) {
throw NoSuchElementException()
}
@Recover
fun recoverByNoSuchElementException(e: NoSuchElementException, input: String) {
println("recover 실행")
}
이제 테스트 코드에서 retryByNoSuchElementException() 을 실행하게 되면 'recover 실행'이 찍히는 것을 확인할 수 있다.
@Test
@DisplayName("backoff 옵션을 통한 timeDelay")
fun retryByNoSuchElementException() {
val input = "test"
retryableService.retryByNoSuchElementException(input)
}
반대로 처음에 작성했던 basicRetry() 테스트 코드를 돌려보면 다음과 같은 exception이 발생하는 것을 확인할 수 있다.
basicRetry()에는 input과 관련된 인자가 없기 때문이다. 이렇듯 @Recover 어노테이션은 exception 인자를 제외한 나머지 인자에 대해서 동일해야 작동하니 이 점 참고하자.