본문 바로가기
infra/network

[Network] Apache HttpClient의 설정으로 알아보는 클라이언트 관점의 네트워크 속성들

by 사바라다 2024. 5. 17.

개요

네트워크에 대해서 꼼꼼하게 이해하고 사용하면 좋겠다라는 생각을 했습니다. 어디서부터 시작할까를 생각했을 때 가장 쉽게 접근하고 사용할 수 있는 영역부터 시작한다면 좋겠다라는 생각으로 정리하며 이글을 써보게 되었습니다. 다른 서버를 호출하는 HttpClient의 설정을 하나하나 알아보며 이 설정들은 어떤것을 뜻하는지 이해해보도록 하겠습니다.

Apache httpClient

오늘 예제로 사용할 코드는 Apache HttpClient 코드입니다. Apache HttpClient는 현재도 관리되고 있는 몇 안되는 java 기반의 http client 중 하나입니다. 저는 해당 라이브러리를 아래의 이유로 사용하고 있습니다.

  • 현재도 메인테이닝되는 중
  • non-blocking IO 지원
  • 코루틴을 명확하게 지원
  • feign과 연동이 가능하여 선언형(declarative)으로 하게 사용가능

오늘 예제에서는 코루틴, non-blocking에 대해서는 빼고 httpClient에 집중합니다.

기본 코드

HttpClient 를 사용하기 위해서는 아래의 의존성이 필요하고 기본 코드는 아래와 같습니다.

implementation 'org.apache.httpcomponents.client5:httpclient5:5.3.1'
val httpClient = HttpClients.custom().build() // 오늘 주요하게 알아볼 라인
val httpGet = HttpGet("http://google.com")
val response = httpClient.execute(httpGet) 

println(response.code) // 200

위 코드는 정상적으로 실행이되며 200 응답을 받아올 수 있습니다. 그렇다면 이제 본격적으로 HttpClient에서 커스텀하게 설정할 수 있는 부분들에 대해서 알아보고 그와 연관된 네트워크 CS까지 심도있게 알아보도록 하겠습니다.

설정 가능 코드

코드는 아래의 형식으로 설정이 가능합니다. 각각의 설정값에 대해서는 아래에서 더 자세하게 알아보도록 하겠습니다. HttpClient는 크게 ConnectionManager와 RequestConfig 2개에 대해서 설정할 수 있습니다.

HttpClients.custom()
    .setConnectionManager(
        PoolingHttpClientConnectionManagerBuilder.create() 
            .setMaxConnPerRoute(5)
            .setMaxConnTotal(25)
            .setConnectionTimeToLive(TimeValue.of(Duration.ofMinutes(3)))
            .build()
    )
    .setDefaultRequestConfig(
        RequestConfig.custom()
            .setConnectionRequestTimeout(5, TimeUnit.SECONDS)
            .setResponseTimeout(10, TimeUnit.SECONDS)
            .setConnectionKeepAlive(TimeValue.of(Duration.ofMillis(2)))
            .build()
    )
    .build()

ConnectionManager

정의

첫번째로 알아볼 설정은 ConnectionManager 입니다. ConnectionManager 클래스는 이름 그대로 client의 Connection을 관리하는 컴포넌트입니다. 이 컴포넌트가 하는 목적은 원격(Remote) 서버에 Http Connection을 요청할 때 어떠한 방식으로 thread와 connection을 연결하여 할당, 요청할지에 대한 방식을 선택할 지에 대한 정의를 해둡니다. 원격 서버와 커넥션을 맺을 때 1개의 thread가 1개의 Connection에 접근할 수 있는데 어떻게 이를 조절할지에 대해서 관리할 수 있습니다. thread-safe 하게 구현되어야합니다. 그리고 멀티 thread에 의해서 공유되는 객체는 동기화 되도록 설계되어야합니다.

다시 설명하면 http 요청이 왔을 때 실제 target과 어떻게 연결하고 client의 thread는 어떻게 활용할 것 인지에 대한 관리를 하는 컴포넌트라고 할 수 있습니다.

PoolingHttpClientConnectionManager와 BasicHttpClientConnectionManager

이러한 ConnectionManager의 실제 구현으로는 PoolingHttpClientConnectionManager와 BasicHttpClientConnectionManager가 있습니다. 기본값은 PoolingHttpClientConnectionManager입니다. PoolingHttpClientConnectionManager은 HttpRoute(Target, path를 제외한 목적지) 별로 Connection을 재사용할 수 있도록 Pool을 만들어서 관리합니다. 여기서 HttpRoute는 target을 Key로 동일한 Target이면 Connection을 재사용할 수 있도록 구현되어있습니다.

MaxConnPerRoute

PoolingHttpClientConnectionManager는 HttpRoute 단위로 Pool을 재활용 할 수 있도록 관리한다고 했습니다. 여기서 HttpRoute 별로 연결할 수 있는 최대 Connection의 수 입니다. 요청을 해야할 때 해당 Pool에서 사용 가능한 리소스가 있는지 확인 후 없으면 생성하고 있다면 재사용할 수 있습니다. 이 때 무한정 Connection을 만들면 리소스적인 손해가 있으므로 이러한 제한을 가지고 있습니다.

MaxConnTotal

MaxConnPerRoute를 통해서 HttpRoute 당 Max Connection을 관리한다고 말씀드렸습니다.뿐만 아니라 전체 Connection에 대해서도 관리해야합니다. 해당 옵션이 바로 전체 Pool에서 전체 Connection 수를 관리하는 옵션입니다.

만약 MaxConnTotal가 넘는다면 가장 오래전에 사용된 Connetion을 release하고 새로운 Connecction을 맺어서 사용할 수 있도록 합니다.

ConnectionTimeToLive

Connection Time To Live 설정은 Connection 연결이 얼마나 오래 살아있는지를 설정하는 연결입니다. 이는 Request 당 Connection을 만들면 생성에 대한 리소스가 아깝고 그렇다고 무한히 살려두면 사용하지 않는 리소스가 아깝기 때문에 Time To Live 시간 동안 더 이상 사용되지 않으면 해당 Connection을 버리는 메커니즘을 위해 존재하는 설정입니다.

Apache HttpClient에서는 created 값과 updated 값과 각각 비교하며 expired 되었다고 판단하면 Connection을 종료시킵니다.

RequestConfig

RequestConfig는 HTTP Request에 적용되는 설정입니다. 이 설정은 Connection Timeout, Keep Alive, 그리고 Response Timeout(Read Timeout)에 대한 내용을 알아두면 좋습니다. 아래에서 각각 알아보도록 하겠습니다.

Connection Timeout

Connection Timeout은 Server와의 통신을 하기 위한 3-way handShake가 완료되고 연결되기 까지의 최대로 기다리는 시간을 나타냅니다. 좀 더 연결하는 입장에서 코드로 디테일하게 본다면 연결의 과정에는 아래와 같은 프로세스가 필요합니다.

@Override
public void connect(...파라미터 생략...) {

    // domain url을 통해서 remote IP Address를 획득
    final InetAddress[] remoteAddresses;
    if (host.getAddress() != null) {
        remoteAddresses = new InetAddress[] { host.getAddress() };
    } else {
        remoteAddresses = this.dnsResolver.resolve(host.getHostName());
    }

    // ... 생략 ...

    for (int i = 0; i < remoteAddresses.length; i++) {
        final InetAddress address = remoteAddresses[i];
        final boolean last = i == remoteAddresses.length - 1;

        // server socket 생성
        Socket sock = sf.createSocket(proxy, context);
        conn.bind(sock); // conn은 ManagedHttpClientConnection

        // remote 정보 생성
        final InetSocketAddress remoteAddress = new InetSocketAddress(address, port);

        try {
            // sock, remote 정보를 통해서 연결 요청
            sock = sf.connectSocket(sock, host, remoteAddress, localAddress, connectTimeout, attachment, context);
            conn.bind(sock);
            conn.setSocketTimeout(soTimeout);
        } catch (final IOException ex) {
            // ...생략...
        }
    }
}

위 코드의 시나리오를 순서적으로 요약하면 아래와 같습니다.

  1. Domain URL 주소를 DNS Resolver를 통해서 IP를 획득
  2. 시스템 Server Socket을 생성
  3. target 서버와 연결(Connection) ( Connection Timeout 사용 )
  4. 연결된 정보를 내 Server Socket에 설정 및 ConnectionManager 설정에 따라 Pool로서 관리할 수 있도록 설정

결과적으로, 연결 과정이라고하면 사실상 1번 ~ 4번의 과정을 거치게됩니다. 하지만 실제적으로 Connection Timeout은 Target한 서버와의 연결에서 기다려줄 수 있는 시간 Timeout 입니다. 이는 Connection 과정 중 remote의 ack를 받지 못하여 발생하는 retransmission이 발생하거나 등의 이슈가 있어 설정한 Timeout 임계치보다 커질 때 발생할 수 있습니다.

KeepAlive

Keep-Alive 설정은 HTTP의 Keep-Alive Header가 없을 경우 Target 서버와 어느정도의 Keep-Alive Connection 시간을 가져갈것인지에 대한 설정입니다. 기본값으로는 3분이고 음수로 설정하면 무한한 시간의 Keep-Alive 시간을 가집니다. KeepAlive를 가져간다는 것은 설정 된 시간동안 Http Connection을 재사용해서 사용할 수 있음을 의미합니다. 이렇게하면 리소스 재사용의 효율을 얻을 수 있습니다. 이는 적절한 시간의 설정이 필요합니다. 각 극단적인 상황에 대해서 발생할 수 있는 단점을 알아봅시다.

  • 너무 짧은 KeepAlive는 Http Connection을 자주 많들어야하기 때문에 이에 리소스를 많이 사용할 수 있습니다.
  • 너무 긴 KeepAlive는 Http Connection의 수가 너무 많아져 리소스를 많이 사용할 수 있습니다.

Response Timeout (Read Timeout)

Response Timeout은 Connection이 맺어진 이후 실제 데이터를 받아오는데 걸리는 시간이 오래걸리면 발생하게됩니다. Read Timeout은 서비스에 맞게 설정하는것이 중요합니다. 이 설정에 고려해야하는 사항은 Target 서버를 갔다오는 RTT(Round Trip Time)과 패킷 드랍이 발생했을 때의 Retransmission이 발생했을 때 RTO(Retransmission Timeout)입니다.

  • RTT 200ms
  • RTO 300ms

라고 할 때 retransmission 이 발생하지 않는다고 가정하면 250ms를 Timeout으로 가져갈 수 있을것입니다. 하지만 retransmission가 1번 있을 수 있음을 가정한다면 RTO로 300ms를 기다리고 재전송되어 500ms만에 결과를 얻어올 수 있습니다. 이렇게 가정하면 적어도 500ms 이상으로 Timeout을 설정해야합니다.

적절한 Timeout의 값은 비지니스에 맞춰 설정할 필요가 있을것입니다.

마무리

오늘은 이렇게 Apache HttpClient의 설정들을 분석하면서 어떠한 구성요소들을 알아두면 좋을지 알아보았습니다.

그 내용으로는 ConnectionManager, ConnectionTimeToLive, ConnectionTimeout, ReadTimeout, KeepAlive 등이 있다는 사실도 알게 되었습니다.

감사합니다.

참고

반응형

댓글