ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • RestTemplate Retry 실습 - RestTemplate Hands On 7
    Spring Framework/RestTemplate 2023. 9. 29. 00:01

    개요

    일시적인 네트워크 이슈로 에러가 발생하는 경우에는 한 번만 실패하고 끝내야 할까?

    이런 경우에는 한번 더 재시도하면 성공할 가능성이 있습니다.

    RestTemplate Retry는 어떻게 가능할까요?

     

    떠오르는 가장 간단한 방법

    @RestController
    class RestTemplateRetryTestController(
        private val restTemplate: RestTemplate,
    ) {
    
        @GetMapping("time-out-retry")
        fun timeOut(){
            val url = "http://localhost:8080/time-out-retry-test"
    
            var responseEntity: ResponseEntity<String>
            try {
                responseEntity = restTemplate.getForEntity(url, String::class.java)
            }catch (e: Exception){
                responseEntity = restTemplate.getForEntity(url, String::class.java)
            }
    
            println(responseEntity)
        }
    
        @GetMapping("time-out-retry-test")
        fun timeOutTest(){
            //현재 timeout은 5초이다
            println("----메서드가 호출됩니다----")
            Thread.sleep(6000)
        }
    }

     

    비록 타임아웃으로 호출은 모두 실패했지만 메서드가 2번 호출됩니다.

     

    결과

    ----메서드가 호출됩니다----
    ----메서드가 호출됩니다----
    
    java.net.SocketTimeoutException: Read timed out

     

    3번 이상 호출해보고 싶다면?

    try{
    	..호출
    }catch(e: Exception){
    	try{
    		..실패시 재 호출
    	}catch(e: Exception){
    		..실패시 재 호출
        }
    }

    위에서 작성한 것처럼 try-catch 내부에 try-catch를 사용할 수도 있습니다.

    그런데 4번 호출하고 싶다면? depth는 점점 더 깊어지게 되고 가독성이 크게 떨어지게 됩니다.

     

     

    반복문과 RunCatching 활용해 보기

        @GetMapping("time-out-retry")
        fun timeOut(){
    
            val result = retryCall()
            if(result.isSuccess){
                println("---good---")
            }
    
            if(result.isFailure){
                logger.warn {result.exceptionOrNull()}
            }
        }
    
        private fun retryCall(): Result<*> {
            val maxRetires = 3
            for (currentRetry in 1..maxRetires) {
                val result = runCatching {
                    restTemplate.getForEntity(url, String::class.java)
                }
                if(result.isSuccess){
                    return result
                }
            }
            return Result.failure<ResponseEntity<String>>(Exception("fail"))
        }

    다른 방법으로는 for문 로직을 작성해서 최대 반복 횟수를 정해놓고 응답값이 성공이면 break 하도록 할 수도 있습니다.

    Kotlin의 runCatching을 같이 활용할 수 있습니다.

    하지만 이런 방법은 Call을 사용하는 로직에 일일이 구현해주어야 합니다.

     

    결과

    ----메서드가 호출됩니다----
    ----메서드가 호출됩니다----
    ----메서드가 호출됩니다----
    2023-09-07T08:41:08.169+09:00  WARN 25186 --- [nio-8080-exec-1] com.example.study.log.Logging            : java.lang.Exception: fail
    
    
    timout을 4초로 잡는다면
    ----메서드가 호출됩니다----
    ---good---

     

    Spring의 Retry를 활용

    dependencies {
        implementation("org.springframework.retry:spring-retry")
        implementation("org.springframework.boot:spring-boot-starter-aop")
    }

     

     

    RetryTemplate 활용하기

    @Configuration
    public class AppConfig {
        
        //...
        
        @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;
        }
    }

    RetryTemplate의 활용하여 2초에 한 번씩, 최대 2회 호출과 같은 재시도 구현정책을 설정할 수 있습니다.

     

    RetryTemplate과 Interceptor 활용

    public class RestTemplateHeaderModifierInterceptor
      implements ClientHttpRequestInterceptor {
    
        @Override
        public ClientHttpResponse intercept(
          HttpRequest request, 
          byte[] body, 
          ClientHttpRequestExecution execution) throws IOException {
     
            ClientHttpResponse response = execution.execute(request, body);
            response.getHeaders().add("Foo", "bar");
            return response;
        }
    }

    ClientHttpRequestInterceptor를 구현하는 인터셉터를 추가하여 request, body, execution 객체에 대해 접근할 수 있습니다.

    인터셉터를 활용하여 응답값(response)에 헤더를 추가하는 예제입니다.

     

    RetryTemplate과 Interceptor를 활용한 코드

    @Configuration
    class RetryRestTemplateConfig{
    
        @Bean
        fun retryRestTemplate(restTemplateBuilder: RestTemplateBuilder): RestTemplate{
            return restTemplateBuilder
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(5))
                .additionalInterceptors(clientHttpRequestInterceptor())
                .build()
        }
        private fun clientHttpRequestInterceptor(): ClientHttpRequestInterceptor {
            return ClientHttpRequestInterceptor { request: HttpRequest, body: ByteArray, execution: ClientHttpRequestExecution ->
    
                //3회 재시도
                val retryTemplate = RetryTemplate()
                retryTemplate.setRetryPolicy(SimpleRetryPolicy(3))
    
                //재시도 2초에 한번
                val fixedBackOffPolicy = FixedBackOffPolicy()
                fixedBackOffPolicy.backOffPeriod = 2000L
                retryTemplate.setBackOffPolicy(fixedBackOffPolicy)
    
                try {
                    return@ClientHttpRequestInterceptor retryTemplate.execute<ClientHttpResponse, IOException> { context: RetryContext? ->
                        execution.execute(
                            request, body
                        )
                    }
                } catch (throwable: Throwable) {
                    throw RuntimeException(throwable)
                }
            }
        }
    }

    additionalInterceptors로 정의한 인터셉터를 추가해 줍니다.

     

    호출 테스트 결과

    ----메서드가 호출됩니다----
    ----메서드가 호출됩니다----
    ----메서드가 호출됩니다----
    2023-09-07T12:39:13.292+09:00 ERROR 26175 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: java.net.SocketTimeoutException: Read timed out] with root cause
    
    java.net.SocketTimeoutException: Read timed out

     

    다음 글에서는 디버깅을 통해 Retry와 Interceptor의 동작과정에 대해 알아보고자 합니다.

     

     

    참고자료

    https://www.baeldung.com/spring-retry

    https://www.baeldung.com/spring-rest-template-interceptor

    댓글

Designed by Tistory.