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 함수는 인자 변수 errortrue이면 에러가 발생하고, errorfalse는 에러가 발생하지 않는다.

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;

}

재시도가 전부 실패하면, RetryOperationsRecoveryCallback을 호출한다. 이 기능을 사용하려면 클라이언트는 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

참조

위에 예제 코드는 GitHub에서 확인해 볼 수 있다.