웹 어플리케이션을 만들면서 꼭 한번 쯤 만나게 되는 “RestTemplate”. 접근 가능한 외부 HTTP URL(보통 API)을 호출하는 방법중에 하나로 springframework 에서 제공해주는 모듈이다. 특히 큰 한덩어리로 관리되던 Monolithic Architecture 에서 요청을 하고(client) 응답을 주는(server) 즉, Endpoint가 작은 단위로 분리되는 Microservice Architecture 로 바뀌면서 각 서비스간 호출방식이 HTTP 일 경우 자주 사용되곤 하는 것 같다. (webClient 등 다른 여러 호출 방법들이 있다.)
만약, 요청을 하는 클라이언트 입장에서 응답을 주는 서버의 상태가 불안정 하다고 가정했을때, 어떤식으로 처리해야 할까? 예컨대, 요청 10번에 한번은 어떠한 이슈로 응답이 지연되거나 서버에러가 발생한다고 하면 클라이언트를 사용하는 사용자 입장에서는 간헐적인 오류응답에 답답함을 호소할 수도 있다. 그럼 잠시 눈을 감고 생각해보자.
가볍게 생각하면 아래처럼 아주 간단하게 “예외처리"를 이용할 수도 있다.
try{// http call
}catch(Exceptione){// 서버에러가 아닌 약속된 에러응답을 리턴
}
하지만 이것도 정답이 아닐수 있는게, “간헐적인 오류"로 인해 사용자는 오류화면을 봐야하기 때문에 클라이언트에 대한 신뢰를 저버릴 수밖에 없다. 그럼 어떻게 해야할까? 여러가지 해결방법이 있겠지만 간단하면서도 강력하다고 생각되는 방법이 바로 “재시도” 라고 생각한다. 클라이언트를 사용하는 사용자가 눈치 못챌만큼 빠르게 재시도를 한다면 에러가 나도 다시한번 호출해서 성공할 수 있는 가능성이 높기 때문이다. (그치만 근본적인 원인은 해결해야…)
이번 포스팅에서는 RestTemplate 를 이용할때 “재시도” 할 수 있는 방법에 대해 알아보고자 한다. 아주 간단할지 모르지만 노력에 비해 효과가 상당하다고 생각하기 때문에 정리해 두고 싶었다.
Spring Retry
공식 Github에 소개를 빌리자면, Spring 어플리케이션에 대한 재시도 지원을 제공한다고 한다. 위에서 이야기 했던 “RestTemplate"과는 사실 무관하고, 이를 활용해서 재시도 하는 “RetryRestTemplate"를 구현해보려 하는것이다. 우선 이 “Spring-Retry"의 예제를 보면 아주 심플하게 사용할 수 있다. 우선 pom에 구현에 필요한 dependency 를 추가하고 아래 코드를 보자.
@EnableRetry 어노테이션을 @Configuration을 지정한 클래스 중 하나에 추가한다.
재시도 하려는 메소드에 @Retryable 어노테이션을 지정해준다.
재시도가 완료되는 시점에서 실행하고 싶을때 선언하는 어노테이션, @Retryable 동일한 클래스에서 선언되어야 하고 return type 은 @Retryable을 지정한 메소드와 동일해야 한다.
Retry Rest Template
이렇게 springframework 에서 제공해주는 spring-retry 를 이용해서 이번 포스팅의 목표인 재시도를 하는 Retry Rest Template 를 구성해보자. 우선, RestTemplate 를 Bean 으로 등록하고, 위에서 이야기 한 어노테이션들로 구성해보자.
SimpleClientHttpRequestFactory 를 만들고 각 타임아웃을 설정해준 다음 RestTemplate 파라미터로 넘겨준다.
사용하는 곳에서 exchange 메소드를 이용할 것이므로 해당 메소드를 오버라이드 해준다. 먼저 해당 메소드에서 “RestClientException"이 발생할 경우 Retry 로직을 수행한다고 정해주고, 최대 시도는 3번, backoff 설정중 delay를 1000ms(1초)로 지정해서 재시도가 진행되도록 해준다.
2 에서 지정한 재시도가 끝나면 (재시도를 전부 다 하면) 해당 메소드를 수행하게 되어있고, 임의로 응답에 지정한 문구를 넘겨준다.
이렇게 하고 실제로 사용하는 로직에서 일부러 잘못된 URL을 호출해 보도록 하자. 그리고서 로그를 자세히 보도록 application.properties 에 “debug=true” 설정을 해준다.
@RestControllerpublicclassTestController{@AutowiredprivateRestTemplateretryableRestTemplate;@GetMapping(value="/employees",produces=MediaType.APPLICATION_JSON_VALUE)@ResponseBodypublicStringemployees()throwsURISyntaxException{finalStringbaseUrl="http://dummy.restapiexample.com/api/v1/employeeszzz";// zzz 가 빠져야 한다.
URIuri=newURI(baseUrl);ResponseEntity<String>exchange=retryableRestTemplate.exchange(uri,HttpMethod.GET,null,String.class);returnexchange.getBody();}}
브라우저에서 해당 url을 접근해보면 @Recover 에서 지정했던 결과를 볼 수 있게 된다. 사실 이러한 방법에 대해 삽질하기 전에 다른 방법들을 찾아봤지만 restTemplate 을 사용하는 곳에서 각각 retry관련 로직을 추가해야 했기에 뭔가 어글리해 보여서 삽질을 시작하게 되었다. 다행히 성공.
마치며
어떻게 보면 너무나 당연하게 “여러번 재시도 요청하면 되지?” 라는 말을 할 수 있지만, “입코딩” 하는 것과 실제로 코드를 구현하는건 다른 이야기인것 같다. 정말 작은 로직 추가로 꽤 큰 효과를 볼 수 있어 다행이라 생각한다.
이러한 “재시도” 말고도 요청하고자 하는 곳의 서버의 상태가 안좋을 때 서버에러가 아닌 다른 명시적인 에러를 반환할 수 있는 방법이 다양할 것 같다. 모든것엔 정답이 없는 것 처럼.
제목에서 알 수 있듯이 다음 “2부” 에서는 “Retry"가 아닌 “Circuit Breaker"를 사용하여 “재시도"의 방법보다 조금 다른 측면에서 조금 더 괜찮은 방법으로 RestTemplate 를 사용해 보고자 한다.