FeignClient에서 read timeout 발생 시 주의사항 (w/ Retry, RetryableException)
Programming

FeignClient에서 read timeout 발생 시 주의사항 (w/ Retry, RetryableException)

1. 알고자 하는 것

  • FeignClient에서의 read timeout 
  • FeignClient 이슈 분석
  • FeignClient에서 read timeout 발생 시 주의사항

 

2. 알게된 것

 

배경

  • 회사에서 MSA 전환을 준비하기 이전 단계로, 일부 이관되어 멀티모듈로 구성되어있는 프로젝트에 대해 FeignClient로 모듈 간 통신을 하며 발생했던 이슈이다.
  • 특정 조건에 해당하는 n건의 데이터에 대해 일괄적으로 배치 처리를 수행하는 Job이 있다.
  • 해당 Job은 1시간 간격으로 수행되며, 한 번 수행 시 평균 약 5-10건의 데이터를 대상으로 수행된다. 
  • Job 내에서 다른 모듈의 API를 FeignClient를 통해 호출하여 추출된 데이터에 대한 비즈니스 로직을 처리한다.
    • 반환값은 따로 없으며, 데이터에 대한 update 동작만을 수행하는 Job이다.
  • Job이 돌기 전, 데이터 부정합 건에 대해 일괄적으로 CS팀에서 데이터 보정을 수행하였다.
  • 이로 인해 갑작스럽게 많은 건의 데이터가 배치 조건에 포함되었고, 데이터 보정 후 수행된 Job에 77건의 데이터에 대한 처리가 진행되었다.
  • 이 때, FeignClient의 read timeout이 5초로 설정되어 있었고, 평소 약 5-10건의 데이터를 대상으로 수행하여 5초 내로 Job이 완료되었던 것과 달리, 77건의 데이터에 대해 일괄적으로 FeignClient를 통해 API를 호출하여 처리하다보니 read timeout이 발생했다.
  • read timeout으로 인해 모니터링 로그에는 RetryableException: Read timed out 에러가 남았다.
  • 하지만, 77건의 데이터에 대해서 정상적으로 처리가 완료되었다.

 

FeignClient에서의 read timeout 

  • FeignClient에서 read timeout 값은 다음과 같은 의미를 가진다.
    • n초 내에 API 호출에 대한 응답이 오지 않으면 Retry 정책에 따라 Retry를 수행하고, Retry 횟수를 초과하면 RetryableException을 발생시킨다.

 

FeignClient 이슈 분석

  • 이 때 중요한 점은, read timeout 발생 시 Transaction처럼 기존 API 호출 동작을 rollback 후 Retry 하는 등의 동작을 수행하는 것이 아니다.
  • 단지, 설정한 read timeout 시간보다 오래걸렸다 = 서버 네트워크에 일시적인 문제가 있을 가능성이 있다라고 생각하고 다시 API를 호출하는 것이다.
  • 따라서, read timeout이 발생했다고 하더라도 기존에 호출했던 API의 동작은 중지되지 않고 별개로 수행되는 것이다.
    • 애초에 다른 서버(모듈)의 API에 대한 동작을 호출한 쪽에서 제어한다는 것이 불가능하다는 점을 생각하면 이해가 편하다.
  • 이러한 read timeout의 동작에 의해 위 배경에서 77건의 데이터에 대해 read timeout이 발생했지만 호출한 API는 수행되어 정상적으로 처리가 완료된 것이다.
  • 단지 read timeout을 77건의 데이터가 모두 처리될만큼 충분한 시간으로 설정하지 않았기에 호출한 쪽에서 RetryableException만 throw 된 것이다.
  • 또한, 프로젝트의 FeignClient Retry 정책이 Retryer.NEVER_RETRY로 설정되어 있어 read timeout 발생 후 retry를 수행하지 않아 중복호출 역시 발생하지 않았다.
  • Retry 정책을 NEVER_RETRY로 설정한 근거는 다음과 같다. 
    • 조회성 API가 아닌 데이터에 대한 update를 수행하는 API이므로 호출한 쪽에서 RetryableException으로 인해 응답을 받지 못하더라도 API의 동작만 모두 수행되면 update 동작은 모두 진행되므로 데이터의 부정합이 발생하지 않는다.
    • 1시간 단위로 수행되는 배치에서 사용되므로, 일시적인 서버 네트워크 장애로 인해 오류가 발생하더라도 1시간 뒤 배치가 재실행되므로 자체적인 Retry가 수행된다. (데이터 처리 누락 X)
    • Retry로 인해 update 동작이 중복호출되어 데이터 부정합이 발생할 수 있다.

 

FeignClient에서 read timeout 발생 시 주의사항

  • 위 배경과 같이 FeignClient를 통해 호출한 API가 단순히 update와 같은 데이터 처리만을 수행하는 API라면 read timeout이 발생해도 API 서버에 문제가 없다면 정상적으로 처리된다.
  • 그러나, 호출한 API에 대해 응답을 받아야 하고, 응답값을 통해 무언가를 한다면 read timeout 시 이후 동작이 올바르게 수행되지 않는다.
  • API가 정상적으로 결과를 반환했다 하더라도, 호출한 쪽에서는 read timeout으로 인해 RetryableException이 발생하여 이후 동작이 처리되지 않기 때문이다.

 

  • 다음과 같이 10초의 수행시간을 가지는 API가 존재한다고 하자.
  • 하나는 반환값이 없이 단순히 데이터를 처리하는 API이며, 다른 하나는 응답값을 반환하는 API이다.
@Service
public class InternalService {

    public void delayedLogicWithoutReturn() throws InterruptedException {
        int a = 100;
        Thread.sleep(10000L);
        for (int i = 0; i < 100; i++) {
            a++;
        }
        System.out.println("result a : " + a);
    }

    public String delayedLogicWithReturn() throws InterruptedException {
        int a = 100;
        Thread.sleep(10000L);
        for (int i = 0; i < 100; i++) {
            a++;
        }
        return "result a : " + a;
    }
}



@RequiredArgsConstructor
@RequestMapping("/api/internal")
@RestController
public class InternalController {

    private final InternalService internalService;

    @GetMapping("/delayed")
    public void delayedWithoutReturn() throws InterruptedException {
        internalService.delayedLogicWithoutReturn();
    }

    @GetMapping("/delayed/return")
    public String delayedWithReturn() throws InterruptedException {
        return internalService.delayedLogicWithReturn();
    }
}

 

  • FeignClient를 통해 해당 API를 호출하는 쪽에서의 read timeout 값은 5초로 설정했다.
  • retry 정책은 NEVER_RETRY로 설정했다. (Retry 수행 X, read timeout 발생 시 바로 RetryableException throw)
public class FeignConfig {

    @Bean
    public Request.Options requestOptions() {
        long connectionTimeout = 10;
        long readTimeout = 5; // read - timeout 5초로 설정
        return new Request.Options(connectionTimeout, TimeUnit.SECONDS, readTimeout, TimeUnit.SECONDS, false);
    }

    @Bean
    public Retryer retryer() {
        return Retryer.NEVER_RETRY;
    }
}

 

  • 단순한 데이터 처리, 반환값을 주는 두 API에 대해 read timeout이 발생하도록 설정하고 FeignClient를 통해 API를 호출한다.
@RequestMapping("/api/external")
@RequiredArgsConstructor
@RestController
public class ExternalController {

    private final ApiClient apiClient;

    @GetMapping("/delayed")
    public String delayedWithoutReturn() throws InterruptedException {
        apiClient.delayedWithoutReturn();
        return "Success!";
    }

    @GetMapping("/delayed/return")
    public String delayedWithReturn() throws InterruptedException {
        return apiClient.delayedWithReturn();
    }
}

 

  • 반환값이 없이 단순히 데이터가 처리되는데에 10초가 소요되는 API의 경우, 호출한 쪽에서는 RetryableException이 발생했지만 API는 10초 뒤 정상적으로 데이터 처리를 수행했다.

API 호출한 쪽 - read timeout 발생하여 이후 로직 동작 X
API - read timeout과 관계없이 10초 간 정상적으로 로직 수행

  • 반면, 데이터를 반환하는 조회성 API의 경우, 호출한 쪽에서 RetryableException이 발생해 API에 대한 응답값을 받지 못하고 이후 동작이 수행되지 않는다.

read timeout으로 인해 API 반환값을 받지 못하고 로직 진행 X

  • 따라서, 조회성 API를 FeignClient로 호출할 때에는 read timeout 발생 시 응답값을 받지 못하여 이후 동작을 수행하지 못한다.
  • 이 때에는 read timeout 값과 Retry 정책을 프로젝트의 상황에 맞춰 적절히 설정하는 것이 필요하다.
    • read timeout 값과 Retry 정책이 필요한 이유이기도 하다.

 

3. 정리

  • FeignClient의 read timeout에 의한 Retry는 단순히 API를 다시 호출하는 것이다.
  • update성 API에 대해서는 Retry로 인한 중복호출과 같은 문제를 주의해야 한다.
  • 반대로 조회성 API에 대해서는 read timeout으로 인한 데이터 응답 실패와 같은 문제를 주의해야한다.
  • 이렇듯, API의 용도와 프로젝트의 조건에 따라 적절하게 read timeout 시간과 Retry 정책을 조절해야 한다.

 


Source Code

https://github.com/HunSeongPark/blog-study/tree/main/feign-client-retry