-
RestTemplate Retry 실습 - RestTemplate Hands On 7Spring 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의 동작과정에 대해 알아보고자 합니다.
참고자료
'Spring Framework > RestTemplate' 카테고리의 다른 글
RestTemplate Retry와 Interceptor 동작원리 - RestTemplate Hands On 8 (0) 2023.09.30 RestTemplate Timeout - ResetTemplate Hands On 7 (0) 2023.09.28 RestTemplate Error Handling - RestTemplate Hands On 6 (0) 2023.09.27 RestTemplate Post Request - RestTemplate Hands On 4 (0) 2023.09.24 RestTemplate Get Request - RestTemplate Hands On 3 (0) 2023.09.23