티스토리 뷰

오랜만의 기술 블로그... 뭔가 조금이긴 하지만 댓글도 달리고 방문자 수도 평일 기준 300~400 선으로 유지되니 다시 관리를 열심히 해봐야겠다.. (현생...!!)

 

 

최근에 회사에서 FeignClient를 사용했다.

FeignClient에 대해 나중에 다시 정리를 해서 글을 올릴테지만 간단하게 설명하면 OkHttpClient, Apache HttpClient와 같은 http client를 손쉽게 쓸 수 있도록 하는 client builder라고 보면 된다. (neflix에서 만들었다.)

http request를 손쉽게 요청하고 받을 수 있는 만큼 connection timeout, read timeout을 설정할 수 있는데 이 설정을 application.properties(yml)으로 설정하는 것이 아닌 bean으로 설정할 경우 몇 가지 주의해야할 점이 있다.

 

 

FeignClient timeout 설정

feignClient는 okHttp, httpClient를 주입해서 사용하는 것으로 request timeout 설정은 주입하는 okHttp, httpClient에 하도록 되어 있다. 이걸 빈으로 설정할 경우 코드는 아래와 같다. (최근 kotlin으로 개발하게 되가지고 코드는 kotlin으로 작성되어 있다.)

 

OkHttpClient

connectPool 설정도 할 수 있지만 일단 여기서는 간단하게 connectTimeout, readTimeout만 설정해보겠다.

@Configuration
@EnableFiengClients(basePackages = ["com.test.feign.api"])
class FeignClientConfig {
    private val connectTimeout = 2000
    private val readTimeout = 5000
    
    ...
    
    @Bean
    fun okHttpClient(): OkHttpClient {
        val httpClientFactory = DefaultOkHttpClientFactory(OkHttpClient.Builder())
        this.okHttpClient = httpClientFactory.createBuilder(false)
            .connectTimeout(Duration.ofMillis(connectTimeout))
            .readTimeout(Duration.ofMillis(readTimeout)).build()
        return okHttpClient
  	}
  	...
 }

 

Apache HttpClient

httpClient에서 readTimeout은 socketTimeout으로 설정한다. readTimeout 설정에 대해서는 각 client마다 socketTimeout으로 쓸 때도 있고 readTimeout으로 슬 때도 있다.

@Configuration
@EnableFiengClients(basePackages = ["com.test.feign.api"])
class FeignClientConfig {
    private val connectTimeout = 2000
    private val readTimeout = 5000
    
    ...
    
    @Bean
    fun customHttpClient(): CloseableHttpClient {
        val requestConfig = RequestConfig.custom()
            .setConnectTimeout(connectTimeout)
            .setSocketTimeout(readTimeout)
            .build()

        this.httpClient = HttpClientBuilder.create()
            .setDefaultRequestConfig(requestConfig)
            .evictExpiredConnections()
            .build()

        return this.httpClient
    }
  	...
 }

 

 

위와 같이 분명 connectTimeout과 readTimeout을 2000, 5000 miilis 로 설정을 해줬다.

하지만 막상 디버깅을 해보면 설정한 값이 안 먹히는 것을 할 수 있다.

이건 각 feign 내부 client의 execute 코드를 까보면 알 수 있다.

 

 

okhttpClient와 httpClient 내부의 execute

feign.okhttp.OkHttpClient

feign okhttpClient 내부의 execute를 보면 아래와 같다.

@Override
public feign.Response execute(feign.Request input, feign.Request.Options options)
      throws IOException {
    okhttp3.OkHttpClient requestScoped;
    if (delegate.connectTimeoutMillis() != options.connectTimeoutMillis()
        || delegate.readTimeoutMillis() != options.readTimeoutMillis()
        || delegate.followRedirects() != options.isFollowRedirects()) {
      requestScoped = delegate.newBuilder()
          .connectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS)
          .readTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS)
          .followRedirects(options.isFollowRedirects())
          .build();
    } else {
      requestScoped = delegate;
    }
    Request request = toOkHttpRequest(input);
    Response response = requestScoped.newCall(request).execute();
    return toFeignResponse(response, input).toBuilder().request(input).build();
}

 

여기서 delegate는 우리가 timeout을 설정해서 넣은 okhttpClient를 의미한다.

그리고 option은 feign.Request.Options를 의미하는 것으로 application.properties(yml) 설정의 timeout을 읽어 생성되는 객체이다. (만일 없을 경우 default로 들어감)

여기서 중요한 것은 if-else 문이다.

 

if-else 문을 보면 delegated의 connectTmeout, readTimeout, followRedirects와 optio의 connectTimeout, readTimeout, followRedirects를 비교해서 세 값이 하나라도 일치하지 않을 경우 option의 값들로 새로 client를 생성하는 것을 볼 수 있다.

if (delegate.connectTimeoutMillis() != options.connectTimeoutMillis()
        || delegate.readTimeoutMillis() != options.readTimeoutMillis()
        || delegate.followRedirects() != options.isFollowRedirects()) {
      requestScoped = delegate.newBuilder()
          .connectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS)
          .readTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS)
          .followRedirects(options.isFollowRedirects())
          .build();
} else {
      requestScoped = delegate;
}

 

결국 okhttpClient bean으로 설정한 connectTimeout, readTimeout의 값이 무시되고 feign.Requet.Option을 따로 설정하지 않았으므로 default 값으로 모두 덮어씌어지는 것이다.

 

feign.httpclient.ApacheHttpClient

그렇다면 httpClient는 어떻게 되어 있을까?

우선 내부로 들어가보면 execute는 아래와 같이 간단하다.

@Override
  public Response execute(Request request, Request.Options options) throws IOException {
    HttpUriRequest httpUriRequest;
    try {
      httpUriRequest = toHttpUriRequest(request, options);
    } catch (URISyntaxException e) {
      throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e);
    }
    HttpResponse httpResponse = client.execute(httpUriRequest);
    return toFeignResponse(httpResponse, request);
  }

 

하지만 여기서 중요한 것은 httpUriRequest를 생성하는 toHttpUriRequest(request, options) 함수 내부이다.

함수 안에서는 header 설정 등 여러 가지 설정을 해서 길긴한데 여기서 우리가 주요하게 봐야하는 timeout 설정하는 부분만 잘라서 보면 아래와 같다.

HttpUriRequest toHttpUriRequest(Request request, Request.Options options)
      throws URISyntaxException {
    RequestBuilder requestBuilder = RequestBuilder.create(request.httpMethod().name());

    // per request timeouts
    RequestConfig requestConfig =
        (client instanceof Configurable ? RequestConfig.copy(((Configurable) client).getConfig())
            : RequestConfig.custom())
                .setConnectTimeout(options.connectTimeoutMillis())
                .setSocketTimeout(options.readTimeoutMillis())
                .build();
    requestBuilder.setConfig(requestConfig);

    ...

    return requestBuilder.build();
  }

 

httpClient 또한 우리가 만든 client에 대해 조건을 달고 사용하고 있다. 여기서는 Configurable을 상속 받은 client인지를 체크한다. 위에서 작성했던 코드대로 httpClient를 생성하면 InternalHttpClient를 생성해서 return 해주므로 Configurable을 상속 받았는지에 대한 조건을 통과한다. 그러면 된거 아닌가? 라고 생각할 수 있지만 삼항 연산자의 괄호가 어디까지인지 잘 봐야한다.

    RequestConfig requestConfig =
        (client instanceof Configurable ? RequestConfig.copy(((Configurable) client).getConfig())
            : RequestConfig.custom())
                .setConnectTimeout(options.connectTimeoutMillis())
                .setSocketTimeout(options.readTimeoutMillis())
                .build();
    requestBuilder.setConfig(requestConfig);

 

괄호까지 잘라보면 아래와 같다. 진짜 헷갈리게 만들어놨다.

즉, 괄호를 제대로 보고 이해하면 Configurable을 상속받은 client일 경우 client를 카피하고 그렇지 않을 경우 새 기본 빌더를 써! 그 빌더에다가 Request.Options의 connectTimeout, readTimeout을 설정한다.

즉, 조건을 통과해도 무조건 우리가 빈으로 생성한 client의 connectTimeout, readTimeout이 아니라 무조건 RequestConfig.Options의 값을 쓰겠다는 것이다.

RequestConfig requestConfig =
        (client instanceof Configurable ? RequestConfig.copy(((Configurable) client).getConfig())
            : RequestConfig.custom())

 

왜 이렇게 되어 있는 걸까?

이쯤 되면 도대체 왜 이렇게 헷갈리게 만들었는지 이해가 되지 않을 것이다. 두 client의 내부는 다르지만 결국 요점은 Request.Options의 connectTimeout, readTimeout 값을 우선시 하겠다는 것이다.

이것을 이해하려면 application.properties(yml)으로 설정하면 아래와 같이 설정할 수 있다. 즉, spring에서는 feign client의 connectTimeout, readTimeout을 받아서 자동으로 설정해준다는 것이다.

feign.client.config.default.connect-timeout= 2000
feign.client.config.default.read-timeout= 2000

 

okhttpClient, apache httpClient는 이러한 spring의 정책을 인지하고 따른 것이라고 볼 수 있다. 그래서 spring에서 application.properties(yml)을 읽어서 자동으로 만들어주는 Request.Options의 값을 우선시 한 것이다.

 

 

어떻게 해결해야 할까

해결 방법은 간단하다.

connectTimeout, readTimeout을 client 빈을 생성하는 곳에 설정하는 것이 아닌 따로 Request.Options 빈을 생성하는 곳에서 설정해주면 된다. 그러면 okHttpclient, apache HttpClient 모두 빈으로 생성된 Request.Options의 connectTimeout, readTimeout의 값을 읽고 사용하게 된다.

@Configuration
@EnableFiengClients(basePackages = ["com.test.feign.api"])
class FeignClientConfig {
    private val connectTimeout = 2000
    private val readTimeout = 5000
    
    ...
    
    @Bean
    fun options(): Request.Options {
        return Request.Options(connectTimeout, TimeUnit.MILLISECONDS, readTimeout, TimeUnit.MILLISECONDS, false)
    }
  	...
 }

 

feign.okhttp.OkHttpClient를 사용할 때의 또 다른 주의 사항

okHttpClient의 경우 feign으로 사용할 때 다른 주의할 점이 하나 더 있다.

에서 봤던 feign.okhttp.OkHttpClient 내부의 execute를 다시 자세히 보면 if-else에서 if(client와 Request.Options의 값이 일치 하지 않는 경우)에서 새로운 빌드를 받아 아예 client를 새로 빌드하는 것을 볼 수 있다.

@Override
public feign.Response execute(feign.Request input, feign.Request.Options options)
      throws IOException {
    okhttp3.OkHttpClient requestScoped;
    if (delegate.connectTimeoutMillis() != options.connectTimeoutMillis()
        || delegate.readTimeoutMillis() != options.readTimeoutMillis()
        || delegate.followRedirects() != options.isFollowRedirects()) {
      requestScoped = delegate.newBuilder()
          .connectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS)
          .readTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS)
          .followRedirects(options.isFollowRedirects())
          .build();
    } else {
      requestScoped = delegate;
    }
    Request request = toOkHttpRequest(input);
    Response response = requestScoped.newCall(request).execute();
    return toFeignResponse(response, input).toBuilder().request(input).build();
}

 

requestConfig를 새로 빌드하는 apache httpClient와는 다르게 매 번 okhttpClient를 새로 빌드하는 것이다. client와 options의 값이 일치할 경우는 client를 그대로 사용하니 문제가 되지 않지만 다를 경우에는 위와 같이 client를 매 번 빌드한다는 것을 인지하고 사용해야할 것이다.

    RequestConfig requestConfig =
        (client instanceof Configurable ? RequestConfig.copy(((Configurable) client).getConfig())
            : RequestConfig.custom())
                .setConnectTimeout(options.connectTimeoutMillis())
                .setSocketTimeout(options.readTimeoutMillis())
                .build();
    requestBuilder.setConfig(requestConfig);

 

 

 

 

반응형

'SpringBoot' 카테고리의 다른 글

JsonSerializer, JsonDeserializer  (1) 2024.02.04
Spring Cloud Gateway Retry Filter  (1) 2024.01.28
Spring AOP @Before, @After, @Around  (0) 2021.10.23
EhCache2로 캐싱 기능 구현해보기  (1) 2021.08.30
Cache 기능 Redis로 구현하기  (0) 2021.08.12
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함