'날씨 알리미' 서비스는 공공API로부터 주기적으로 날씨 데이터를 얻어와야 합니다. 그런데 공공API 서버에 문제가 있거나 간헐적으로 문제가 발생해 응답이 오지 않는 경우에 대비해야한다고 생각되었습니다. 그래서 특정 시간 동안 응답이 오지 않으면 재시도하는 로직과 최대 재시도 횟수를 정해야겠다고 생각했습니다.
메시지큐에서 메시지가 제대로 처리되지 않는 경우는 직접 예외를 던지고 실패 횟수를 직접 관리하며 처리했었지만, 스프링에서 제공하는 @Retryable 애너테이션을 활용하는 방법도 있어 적용해보았습니다.
먼저 아래와 같이 설정을 해줍니다.
implementation 'org.springframework:spring-aspects'
implementation 'org.springframework.retry:spring-retry'
@Configuration
@EnableRetry // @SpringApplication에 적어줘도 무방합니다.
public class RetryConfig {
}
재시도하고 싶은 메서드에 @Retryable 애너테이션을 붙여주면 됩니다.
value
: retry 예외 대상
maxAttempts
: 재시도 횟수
backoff
: 재시도 사이의 시간 간격
@Recover는 @Retryable 세팅에 의해 재시도를 했음에도 불구하고 실패했을 경우 후처리를 담당합니다. @Recover 메서드는 @Retryable 메서드의 반환타입이 일치해야 하며, 첫 인자로는 예외, 다음 인자로는 @Retryable 메서드의 인자 타입과 일치해야 합니다.
@Retryable(
value = RuntimeException.class, // retry 예외 대상
maxAttempts = 3, // 3회 시도
backoff = @Backoff(delay = 2000) // 재시도 시 2초 후 시도
)
public int callApi(){ ... }
// target 메서드와 반환값 일치
// 메서드의 첫 인자는 예외, 이후 인자는 타겟 메서드와 타입을 일치 시켜야함
@Recover
public int recover() {...}
특정 메서드에 @Retryable을 붙여 처리할수도 있지만, 빈을 이용해 전역적으로 활용할 수도 있습니다.
제가 했던 방법은 RestTemplate 빈을 만들고 exchange 메서드를 override해서 @Retryable을 붙여주었던 것입니다. 그럼 그 restTemplate 빈의 exchange 메서드는 재시도를 지원하게되고, 어느 곳에서든 주입받아 사용할 수 있습니다.
아래 코드처럼 타임아웃 시간과 재시도 횟수, Recover 로직을 설정해놓고, 기존 RestTemplate 대신 이걸 사용함으로써 재시도가 필요할만한 API 요청에 일관되게 적용되도록 했습니다.
아주 간단하죠?
@EnableRetry
@Configuration
public class RetryableRestTemplateConfiguration {
@Bean(name = "restTemplate")
public RestTemplate retryableRestTemplate() {
SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory();
clientHttpRequestFactory.setReadTimeout(20000);
clientHttpRequestFactory.setConnectTimeout(5000);
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory) {
@Override
@Retryable(value = RestClientException.class, maxAttempts = 3, backoff = @Backoff(delay = 60000))
public <T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType)
throws RestClientException {
return super.exchange(url, method, requestEntity, responseType);
}
@Recover
public <T> ResponseEntity<T> exchangeRecover(RestClientException e) {
throw new OpenApiException(OpenApiResponseStatus.UNKNOWN_ERROR.getCode());
}
};
return restTemplate;
}
}
CRUD 중 CUD 요청은 비멱등합니다. 즉, 처리가 완벽히 처리되지 않았더라도 어느정도는 처리가 되어 상대서버에 반영이 됐을 수도 있습니다. 그래서 재시도를 함부로 하면 문제가 발생할 수 있습니다.
대신 READ 요청의 경우 멱등하기 때문에 큰 문제가 발생하지 않으므로 몇 번의 재시도를 하는 것은 무방하다고 할 수 있습니다. 그러나 일시적인 오류가 아닌 지속적으로 오류가 발생한다면 재시도처리 말고 근본적인 원인을 찾아 해결해야겠죠.
메시지큐를 구현하면서 재시도 로직을 직접 짰을땐 꽤 번거로웠는데, @Retryable을 사용하니 훨씬 간단해지고 일관적이어서 편했습니다.