레이어 | 방안 | 장점 | 단점 | 비고 |
---|---|---|---|---|
비즈니스 | Spring Retry | 어노테이션 기반의 Retry 제공 | @Retryable, @Recover, RetryTemplate | |
Resilience4j Retry | 어노테이션 기반의 Retry 제공 Resilience4j CircuitBreaker 를 사용하는 경우, 도입하기 편리함 metrics 연동 제공(resilience4j-micrometer) | 도입하기 위해 Resilience4j 에 대한 디펜던시가 추가됨 | @Retry 인스턴스 기반 Retry | |
API IO | RestTemplate - RetryTemplate | 자세하게 설정 가능 | 어노테이션, 프로퍼티 기반에 비해 편리하게 도입하기 어려움 | RestTemplate + ClientHttpRequestInterceptor + RetryTemplate |
Spring Cloud Commons - Loadbalancer Retry | 프로퍼티 기반으로 Retry 설정 가능 | 제한된 상황에 대해서만 Retry 가능 | RetryableFeignBlockingLoadBalancerClient | |
Spring Cloud OpenFeign - Feign Retryer | 자세하게 설정 가능 | 구현체 코드 작성 필요 | Feing Retryer |
RetryTemplate
을 이용하여 Retry 처리가 가능하나 대부분의 경우 @Retryable
, @Recover
어노테이션을 통해 처리 가능합니다.import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.annotation.Recover;
@Service
public interface MyService {
// RuntimeException 에 대해 최대 Retry 1회 시도
@Retryable(value = RuntimeException.class, maxAttempts = 1)
void retryService(String param);
// Retry 를 했으나 실패한 경우, recover 수행
@Recover
void recover(RuntimeException e, String param);
}
RetryPolicy
: 재시도 정책, BackOffPolicy
: 재시도 주기 정책), RetryTemplate
을 사용할수 있습니다.import org.springframework.retry.support.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;
}
}
import io.github.resilience4j.retry.RetryRegistry;
import io.github.resilience4j.retry.Retry;
@Service
public class MyServiceImpl {
public void retryService(String param) {
// 현업에서는 RetryRegistry 와 Retry 를 Bean 으로 등록하여 사용 필요!
RetryConfig config = RetryConfig.custom()
.maxAttempts(2)
.waitDuration(Duration.ofMillis(1000))
.retryOnResult(response -> response.getStatus() == 500)
.retryOnException(e -> e instanceof WebServiceException)
.retryExceptions(IOException.class, TimeoutException.class)
.ignoreExceptions(BusinessException.class, OtherBusinessException.class)
.failAfterMaxAttempts(true)
.build();
RetryRegistry registry = RetryRegistry.of(config);
Retry retryWithDefaultConfig = registry.retry("name1");
retryWithDefaultConfig.executeRunnable(() -> this.doRetryService(param));
}
}
io.github.resilience4j:resilience4j-spring-boot2
혹은 io.github.resilience4j:resilience4j-spring-cloud2
(RefreshScope 지원) 와 연동하는 경우 어노테이션 + 프로퍼티 기반으로 설정 가능합니다.import io.github.resilience4j.retry.annotation.Retry;
@Service
public interface MyService {
// myRetry 인스턴스를 사용하여 Retry 수행, 실패시 fallback 호출
@Retry(name = "myRetry", fallbackMethod = "fallback")
void retryService(String param);
// Retry 를 했으나 실패한 경우, fallback 수행
void fallback(RuntimeException e);
}
RestTemplate
+ ClientHttpRequestInterceptor
+ RetryTemplate
을 통해 Retry 로직을 수행할수 있습니다.import org.springframework.boot.web.client.RestTemplateCustomizer;
import org.springframework.http.client.ClientHttpRequestInterceptor;
@Configuration
public class RestTemplateConfiguration {
// RestTemplateAutoConfiguration#restTemplateBuilderConfigurer 에서
// RestTemplateBuilder 생성시 사용됨
@Bean
public RestTemplateCustomizer restTemplateCustomizer(ClientHttpRequestInterceptor interceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
list.add(interceptor);
restTemplate.setInterceptors(list);
};
}
// Retry 로직이 추가된 인터셉터
@Bean
public ClientHttpRequestInterceptor clientHttpRequestInterceptor() {
return (request, body, execution) -> {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(3));
try {
return retryTemplate.execute(context -> execution.execute(request, body));
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
};
}
}
org.springframework.http.client.ClientHttpRequestInterceptor
: 요청에 대한 인터셉터org.springframework.http.client.InterceptingClientHttpRequest
: 실행시, 내부적으로 인터셉터 수행org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor
org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient
RetryTemplate
을 이용하여 Retry 수행@FeignClient(name = "some-service")
public interface MyFeignClient {
...
}
spring.cloud.loadbalancer:
retry:
enabled: true
retryable-status-codes: 503 # 503 응답 코드에 대해 재시도 수행
max-retries-on-next-service-instance: 1
clients:
some-service:
retry:
enabled: false # 재시도 수행 X
feign.RetryableException
이 발생한 경우에 대해서 Retry 를 시도할수 있습니다.RetryableException
으로 처리됩니다.RetryableException
을 적용하고 싶은 경우,Decoder
, ErrorDecoder
구현 및 등록이 필요합니다.import feign.Retryer;
public class FeignConfig {
@Bean
public Retryer retryer() {
return new Retryer.Default(100, 1000, 1);
}
}
@FeignClient(name = "some-service", configuration = FeignConfig.class)
public interface MyFeignClient {
...
}
spring.cloud.loadbalancer.retry.enabled: false
# API 요청 동작 순서
example.my.service.MyFeignClient#callApi
-> feign.SynchronousMethodHandler # 디코딩, 에러 디코딩, 재시도(feign.Retryer) 수행
-> org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient # 서비스 디스커버리에 등록된 서버에 대해 로드밸런싱 수행
-> feign.Client # 실제 요청 수행, connectTimeout, readTimeout 이 적용됨
feign.SynchronousMethodHandler
: 전체 수행하는 흐름 파악 가능feign.codec.Decoder
: 응답에 대한 디코딩 수행feign.codec.ErrorDecoder
: 에러 응답에 대한 디코딩 수행feign.Retryer
: 재시도 로직 수행Spring-Retry
적용을 추천드립니다.