Spring Retry
개요
Kotlin 언어를 이용하여 간단한 Spring Retry 활용한 프로젝트를 만들어 보겠다.
Spring Retry는 메소드를 호출해서 예외가 발생 했을 때, 자동으로 지정한 메소드를 다시 호출하는 기능을 제공한다.
일시적인 네트워크 접속 장애가 발생했을 때, Failover으로 유용하다.
Failover: 시스템 대체 작동.
평소 사용하는 서버와 그 서버의 클론 서버를 가지고 있다가 사용 서버가 장애로 사용이 어렵게 되었을 경우 클론 서버로 그 일을 대신하게 해서 무정지 시스템을 구축하게 해주는 것을 의미한다.
프로젝트 생성
아래와 같이 curl
명령어를 사용하여 Spring Boot 초기 프로젝트를 생성한다.
curl https://start.spring.io/starter.tgz \
-d bootVersion=2.7.2 \
-d baseDir=spring-retry \
-d groupId=com.devkuma \
-d artifactId=spring-retry \
-d packageName=com.devkuma.retry \
-d applicationName=RetryApplication \
-d packaging=jar \
-d language=kotlin \
-d javaVersion=11 \
-d type=gradle-project | tar -xzvf -
위 명령어를 실행하게 되면 Java 11, Spring Boot 버전은 2.7.2로 web 프로젝트가 생성된다.
Retry 설정
빌드 스크립트
빌드 스크립트에 Retry을 동작시키기 위한 라이브러리를 아래와 같이 추가한다.
/build.gradle.kts
dependencies {
// ... 중간 생략 ...
implementation("org.springframework.retry:spring-retry:1.3.3")
implementation("org.springframework:spring-aspects:5.3.22")
implementation("io.github.microutils:kotlin-logging:2.1.23")
// ... 중간 생략 ...
}
- spring-retry : Retry 라이브러리
- spring-aspects : AOP 관련 라이브러리
- kotlin-logging : 동작을 확인해 보기 위해 로그 라이브러리
Retry 설정 클래스 추가
신규로 RetryConfig
설정 클래스를 생성해서 아래와 같이 작성한다.
/src/main/kotlin/com/devkuma/retry/config/RetryConfig.kt
package com.devkuma.retry.config
import org.springframework.context.annotation.Configuration
import org.springframework.retry.annotation.EnableRetry
@EnableRetry
@Configuration
class RetryConfig
Retry 적용 관련 클래스 생성
Retry를 적용된 Service 인터페이스 및 클래스 추가
/src/main/kotlin/com/devkuma/retry/service/RetryService.kt
package com.devkuma.retry.service
import org.springframework.retry.annotation.Backoff
import org.springframework.retry.annotation.Retryable
import java.sql.SQLException
interface RetryService {
@Retryable(value = [SQLException::class], maxAttempts = 2, backoff = Backoff(delay = 1000))
fun retry(error: Boolean) : String
fun templateRetry(error: Boolean) : String
}
/src/main/kotlin/com/devkuma/retry/service/impl/RetryServiceImpl.kt
package com.devkuma.retry.service.impl
import com.devkuma.retry.service.RetryService
import mu.KotlinLogging
import org.springframework.retry.annotation.Recover
import java.sql.SQLException
private val log = KotlinLogging.logger {}
class RetryServiceImpl : RetryService {
override fun retry(error: Boolean) : String {
log.info { "Retry called. error=$error" }
if (error)
throw SQLException("retry SQLException")
return "Success"
}
@Recover
fun recover(exception: SQLException, error: Boolean) : String {
log.info { "Recover called: message=${exception.message}"}
return "Success"
}
}
@Retryable
어노테이션을 선언된retry
함수는SQLException
을 발생하였을 때 재시도를 한다. 최대 2번의 재시도를 1000 millisecond의 간격을 두고 시도한다. 2번 시도에대 안되면@Recover
어노테이션을 설정한recover
함수가 실행된다.retry
함수는 인자 변수error
가true
이면 에러가 발생하고,error
가false
는 에러가 발생하지 않는다.
Retry 테스트 코드로 확인
Retry 테스트 코드 클래스 추가
동작을 하기 위해 아래와 같이 테스트 코드를 작성한다.
/src/test/kotlin/com/devkuma/retry/service/RetryServiceTest.kt
package com.devkuma.retry.service
import com.devkuma.retry.config.RetryConfig
import com.devkuma.retry.service.impl.RetryServiceImpl
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest(
classes = [
RetryServiceImpl::class,
RetryConfig::class
]
)
class RetryServiceTest {
@Autowired
private lateinit var retryService: RetryService
@Test
fun `retry error=true`(){
// Given
// When
var result = retryService.retry(true)
// Then
Assertions.assertEquals("Success", result)
}
@Test
fun `retry error=false`(){
var result = retryService.retry(false)
Assertions.assertEquals("Success", result)
}
}
Retry 테스트 코드 실행 확인
작성된 테스트 코드를 실행하면 아래와 같이 동작한다.
Retry 에러가 발생하였을 때
retry error=true
함수가 호출되었을 때의 결과는 아래와 같다.
2022-08-11 18:20:55.787 INFO 13102 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Retry called. param error=true
2022-08-11 18:20:56.792 INFO 13102 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Retry called. param error=true
2022-08-11 18:20:56.792 INFO 13102 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Recover called: message=retry SQLException, param error=true
Retry 에러가 발생하지 않았을 때
retry error=false
함수가 호출되었을 때의 결과는 아래와 같다.
2022-08-11 18:20:56.801 INFO 13102 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Retry called. param error=false
RetryTemplate 설정
RetryOperations 인터페이스
Spring Retry는 일련의 excute()
메소드를 제공하는 RetryOperations
인터페이스를 제공한다.
package org.springframework.retry;
import org.springframework.retry.support.DefaultRetryState;
public interface RetryOperations {
// ... 중간 생략 ...
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback)
throws E;
// ... 중간 생략 ...
}
execute()
의 매개 변수인 RetryCallback
은 실패할 시에, 재시도해야 하는 비즈니스 로직 삽입을 하는 인터페이스이다.
package org.springframework.retry;
public interface RetryCallback<T, E extends Throwable> {
T doWithRetry(RetryContext context) throws E;
}
재시도가 전부 실패하면, RetryOperations
는 RecoveryCallback
을 호출한다. 이 기능을 사용하려면 클라이언트는 execute()
메소드를 호출할 때, RecoveryCallback
객체를 전달해주어야 한다.
package org.springframework.retry;
public interface RecoveryCallback<T> {
T recover(RetryContext context) throws Exception;
}
RetryOperations 구현체
RetryTemplate은 RetryOperations의 구현체이다. @Configuration
클래스에서 RetryTemplate Bean을 구성해 보겠다.
/src/main/kotlin/com/devkuma/retry/config/RetryConfig.kt
package com.devkuma.retry.config
import com.devkuma.retry.support.DefaultListenerSupport
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.retry.annotation.EnableRetry
import org.springframework.retry.backoff.FixedBackOffPolicy
import org.springframework.retry.policy.SimpleRetryPolicy
import org.springframework.retry.support.RetryTemplate
@EnableRetry
@Configuration
class RetryConfig {
@Bean
fun retryTemplate(): RetryTemplate {
val fixedBackOffPolicy = FixedBackOffPolicy()
fixedBackOffPolicy.backOffPeriod = 2000L
val retryPolicy = SimpleRetryPolicy()
retryPolicy.maxAttempts = 3
val retryTemplate = RetryTemplate()
retryTemplate.setBackOffPolicy(fixedBackOffPolicy)
retryTemplate.setRetryPolicy(retryPolicy)
retryTemplate.registerListener(DefaultListenerSupport())
return retryTemplate
}
}
RetryPolicy
는 작업 재시도 횟수를 설정한다.SimpleRetryPolicy
는 고정된 횟수만큼 재 시도하는 데 사용된다.BackOffPolicy
는 재시도 하는 백오프를 제어하는데 사용된다.FixedBackOffPolicy
는 계속하기 전에 일정 시간 동안 일시 중지한다.
RetryTemplate 테스트 코드로 확인
RetryTemplate 테스트 클래스 생성
기존 테스트 코드에 templateRetry
만 추가하였다.
/src/test/kotlin/com/devkuma/retry/service/RetryServiceTest.kt
package com.devkuma.retry.service
// ... 중간 생략 ...
class RetryServiceTest {
@Autowired
private lateinit var retryService: RetryService
@Autowired
private lateinit var retryTemplate: RetryTemplate
// ... 중간 생략 ...
@Test
fun templateRetry() {
retryTemplate.execute<Any, SQLException>(
{ retryService.templateRetry(true) },
{ retryService.templateRetry(false) }
)
}
}
RetryTemplate 테스트 실행 확인
TemplateRetry 에러가 발생하지 않앗을 때
templateRetry
함수가 호출되었을 때의 결과는 아래와 같다.
2022-08-11 18:21:38.192 INFO 13197 --- [ Test worker] c.d.r.support.DefaultListenerSupport : onOpen
2022-08-11 18:21:41.384 INFO 13197 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Retry called. param error=true
2022-08-11 18:21:42.389 INFO 13197 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Retry called. param error=true
2022-08-11 18:21:42.398 INFO 13197 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Recover called: message=retry SQLException, param error=true
2022-08-11 18:21:44.146 INFO 13197 --- [ Test worker] c.d.r.support.DefaultListenerSupport : onClose
참조
- Spring Retry | Java Development Journal
- Guide to Spring Retry | Baeldung
- Spring Retry Review - 아빠프로그래머의 좌충우돌 개발하기!
- spring-projects/spring-retry | GitHub
위에 예제 코드는 GitHub에서 확인해 볼 수 있다.