Spring Boot 에서 API 재시도를 처리할수 있는 여러가지 방안들

정원식·2022년 9월 25일
3
post-thumbnail

개요

  • API 호출에 대한 Retry 를 개선하면서 정리한 여러가지 재시도 방안에 대해 공유합니다.
  • 비즈니스 로직과 관련된 Retry 와 API IO 로직과 관련된 Retry 방안에 대해 참조할수 있습니다.
  • RestTemplate 과 OpenFeign 두가지 API 호출에 대한 Retry 방안을 공유합니다.
  • 기본 환경은 Spring Boot 2(+ Spring Cloud) 로 가정합니다.

방안

레이어방안장점단점비고
비즈니스Spring Retry어노테이션 기반의 Retry 제공@Retryable, @Recover, RetryTemplate
Resilience4j Retry어노테이션 기반의 Retry 제공
Resilience4j CircuitBreaker 를 사용하는 경우, 도입하기 편리함
metrics 연동 제공(resilience4j-micrometer)
도입하기 위해 Resilience4j 에 대한 디펜던시가 추가됨@Retry
인스턴스 기반 Retry
API IORestTemplate - RetryTemplate자세하게 설정 가능어노테이션, 프로퍼티 기반에 비해 편리하게 도입하기 어려움RestTemplate + ClientHttpRequestInterceptor + RetryTemplate
Spring Cloud Commons - Loadbalancer Retry프로퍼티 기반으로 Retry 설정 가능제한된 상황에 대해서만 Retry 가능RetryableFeignBlockingLoadBalancerClient
Spring Cloud OpenFeign - Feign Retryer자세하게 설정 가능구현체 코드 작성 필요Feing Retryer

비즈니스 로직: Spring Retry

  • Spring Retry 를 디펜던시에 추가하여 비즈니스 로직에 대한 Retry 를 편리하게 처리할수 있습니다.
  • 기본적으로 RetryTemplate 을 이용하여 Retry 처리가 가능하나 대부분의 경우 @Retryable, @Recover 어노테이션을 통해 처리 가능합니다.

@Retrayable + @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); 
}

RetryTemplate

  • 보다 정밀하게 설정이 필요한 경우 (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;
    }
}

비즈니스 로직: Resilience4j Retry

  • Resilience4j CircuitBreaker 를 이용하는 경우, 비교적 쉽게 Resilience4j Retry 를 도입할수 있습니다.

Retry

  • Retry 인스턴스를 설정 및 생성하여 Retry 를 시도합니다.
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));
    }
}

@Retry

  • 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); 
}

API IO: RestTemplate + RetryTemplate

  • RestTemplate 에 Retry 를 적용하는 경우, RestTemplate + ClientHttpRequestInterceptor + RetryTemplate 을 통해 Retry 로직을 수행할수 있습니다.
  • IO 예외가 발생한 경우뿐만 아니라 응답 바디, 응답 코드 등에 에러가 있는 경우에 대해서도 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);
            }
        };
    }
}

참조

API IO: Spring Cloud Commons - Loadbalancer Retry

  • Spring Cloud Loadbalancer 환경에서 적용됩니다.
  • 실행도중 IO 예외가 발생하거나, 지정한 응답코드인 경우에 대해서만 Retry 를 시도할수 있습니다.
  • 구현체
    • RestTemplate: org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor
    • OpenFeign: 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

API IO: Spring Cloud OpenFeign - Feign Retryer

  • 실행도중 feign.RetryableException 이 발생한 경우에 대해서 Retry 를 시도할수 있습니다.
  • 기본적으로 IO 예외에 대해서는 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 {
    ...
}

with Spring Cloud Commons Loadbalancer

  • Spring Cloud Loadbalancer 가 작동되는 환경에 있는 경우, 재시도가 중복수행 되지 않기 위해 Spring Cloud Loadabalancer Retry 옵션을 명시적으로 꺼주셔야합니다.
spring.cloud.loadbalancer.retry.enabled: false
  • Retry 를 시도하지만 동일한 서버에 재요청할수 있다는 한계가 존재합니다.
# 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 어노테이션 혹은 Resilience4j 어노테이션을 API IO 로직에 적용 가능합니다.
    • RestTemplate
      • RestTemplate 상속 + 사용하고자 하는 퍼블릭 메서드에 어노테이션 적용
    • Spring Cloud OpenFeign
      • 클라이언트 인터페이스의 메서드에 어노테이션 적용
  • 하지만 AOP 적으로 적용되므로 디코딩 및 예외 핸들링이 완료된 이후에 Retry 로직이 수행됩니다.
  • 간단하게 적용이 필요한 경우에만 적용할것을 권장드립니다.

결론

비즈니스 로직

  • Resilience4j 에 대한 디펜던시가 설정된 경우 (CircuitBreaker 를 사용하는 경우), Resilience4j Retry 를 사용할것을 추천드립니다.
    • 인스턴스 기반의 재시도 환경에 비교적 편리하게 적용 가능할것으로 보입니다.
  • 그렇지 않다면 편리하게 도입 가능하면서도 세밀하게 설정 가능한 Spring-Retry 적용을 추천드립니다.

API 로직

  • RestTemplate 을 사용하는 경우
    • RestTemplate + ClientHttpRequestInterceptor + RetryTemplate 사용할것을 추천드립니다.
  • Spring Cloud OpenFeign 을 사용하는 경우
    • 간단하게 도입이 필요한 경우
      • Loadbalancer Retry 프로퍼티 사용을 추천드립니다.
    • Loadbalancer Retry 프로퍼티만으로는 부족한 경우
      • Feign Retryer 사용을 추천드립니다.

앞서 제시한 방안들과 더불어 재시도를 수행하는 다른 방안에 대해서 알고 계시다면 댓글로 공유부탁드립니다!

Reference

Spring Retry

Resilience4j Retry

RestTemplate + RetryTemplate

Spring Cloud Commons - Loadbalancer Retry

Spring Cloud OpenFeign - Feign Retry

profile
매일매일 성장하고 싶은 백엔드 개발자입니다.

0개의 댓글