티스토리 뷰

Spring 에서 제공하는 http client로는 대표적으로 RestTemplate이 있다. 이 RestTemplate으로 http 요청을 날리게되면 기본적으로 그때마다 connection을 맺고 응답을 받으면 끊게된다. 이를 db connection pool 처럼 connection pool을 만들어서 관리할 수 있다.

 

이를 설정하기위해서는 일단 RestTemplate에 대해서 먼저 이해를 해야하는데 Spring 에서 제공하는 RestTemplate은 직접 http 요청을 하는 역할을 수행하지않는다. 직접 수행하는 클래스를 한번 래핑한 어댑터 역할을 하는 클래스이다. 기본적으로는 jdk에서 제공하는 HttpUrlConnection 클래스를 이용한다. 우리는 apache 에서 제공하는 HttpClient 클래스를 이용하려한다.

 

/**                                                                                                       
 * Create a new instance of the {@link RestTemplate} based on the given {@link ClientHttpRequestFactory}. 
 * @param requestFactory the HTTP request factory to use                                                  
 * @see org.springframework.http.client.SimpleClientHttpRequestFactory                                    
 * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory                            
 */                                                                                                       
public RestTemplate(ClientHttpRequestFactory requestFactory) {                                            
	this();                                                                                               
	setRequestFactory(requestFactory);                                                                    
}                                                                                                         

 

RestTemplate을 보면 디폴트 생성자 외에 ClientHttpRequestFactory 인터페이스를 받는 생성자가있다. javadoc에 나와있는대로 SimpleClientHttprequestFactory와 HttpComponentsClientHttpRequestFactory가 대표적인 구현체이다.

 

클래스명만봐도 알겠지만 따로 인자를 보내주지않으면 기본적으로 SimpleClientHttpRequestFactory를 사용하게된다. 그리고 이게 위에서 말한 jdk의 HttpUrlConnection을 이용하는것이다.

 

우리는 HttpComponentsClientHttpRequestFactory를 보내줘야한다. 이 객체를 생성하자.

 

val factory = HttpComponentsClientHttpRequestFactory(httpClient)
factory.setConnectTimeout(300)                                  
factory.setReadTimeout(300)                                     
                                                                
restTemplate = RestTemplate(factory)                            

(코틀린 코드인점을 감안해서 보자. 생성자를 이용해 객체를 생성하는 구문이다.)

 

HttpComponentsClientHttpRequestFactory 를 이용해서 RestTemplate의 timeout 설정들도 해줄 수 있다. 그런데 우리가 원하는 connection pool 설정은 아직 없다. 잘보면 생성자를 호출할때 httpClient 라는 변수를 인자로 보내는걸 알 수 있는데 이녀석이 핵심이다.

 

/**                                                                           
 * Create a new instance of the {@code HttpComponentsClientHttpRequestFactory}
 * with a default {@link HttpClient} based on system properties.              
 */                                                                           
public HttpComponentsClientHttpRequestFactory() {                             
	this.httpClient = HttpClients.createSystem();                             
}                                                                             
                                                                              
/**                                                                           
 * Create a new instance of the {@code HttpComponentsClientHttpRequestFactory}
 * with the given {@link HttpClient} instance.                                
 * @param httpClient the HttpClient instance to use for this request factory  
 */                                                                           
public HttpComponentsClientHttpRequestFactory(HttpClient httpClient) {        
	this.httpClient = httpClient;                                             
}                                                                             

 

생성자를 따라가보면 HttpClient를 매개변수로 받는걸 알 수 있다. 그리고 위에 디폴트 생성자를 보면 createSystem() 이라는 팩토리 메서드를 이용하는걸 볼 수 있는데 이경우 기본 system properties를 이용해서 HttpClient 객체를 생성하게된다.

 

우리는 이 HttpClient 객체를 만들어야하는것이다. HttpClient 객체를 만들기위한 유틸클래스로 HttpClients 가 있다.

 

/**
 * Factory methods for {@link CloseableHttpClient} instances.
 * @since 4.3
 */
public class HttpClients {

    private HttpClients() {
        super();
    }

    /**
     * Creates builder object for construction of custom
     * {@link CloseableHttpClient} instances.
     */
    public static HttpClientBuilder custom() {
        return HttpClientBuilder.create();
    }

    /**
     * Creates {@link CloseableHttpClient} instance with default
     * configuration.
     */
    public static CloseableHttpClient createDefault() {
        return HttpClientBuilder.create().build();
    }

    /**
     * Creates {@link CloseableHttpClient} instance with default
     * configuration based on system properties.
     */
    public static CloseableHttpClient createSystem() {
        return HttpClientBuilder.create().useSystemProperties().build();
    }

    /**
     * Creates {@link CloseableHttpClient} instance that implements
     * the most basic HTTP protocol support.
     */
    public static CloseableHttpClient createMinimal() {
        return new MinimalHttpClient(new PoolingHttpClientConnectionManager());
    }

    /**
     * Creates {@link CloseableHttpClient} instance that implements
     * the most basic HTTP protocol support.
     */
    public static CloseableHttpClient createMinimal(final HttpClientConnectionManager connManager) {
        return new MinimalHttpClient(connManager);
    }

}

 

몇가지 팩토리 메서드들을 제공하고있는데 우리는 custom 설정을해서 사용할것이므로 custom() 메서드를 호출해서 Builder 객체를 이용하면된다.

 

val httpClient: HttpClient = HttpClients.custom()                 
        .setMaxConnTotal(120)                                     
        .setMaxConnPerRoute(60)                                   
        .build()                                                  
                                                                  
val factory = HttpComponentsClientHttpRequestFactory(httpClient)  
factory.setConnectTimeout(300)                                    
factory.setReadTimeout(300)                                       
                                                                  
restTemplate = RestTemplate(factory)                              

 

이런 형태로 생성해주면된다. maxConnTotal은 연결을 유지할 최대 숫자이고 maxConnPerRoute는 특정 경로당 최대 숫자이다. 이 외에도 각종 설정들을 제어할 수 있으니 공식 홈페이지를 참고해서 설정을 해주면된다.

 

#2019.05.25 내용 추가

포스팅을 작성하고 문득 생각이 들었다. 커넥션 풀이라 함은 미리 연결을 유지하고있다는 의미인데 http 에서 연결을 유지하고있으려면 서버에서 keep-alive 를 지원하고있어야하는거 아닌가? 라는 의문이 든것이다. 마침 나랑 같은 고민을 하신분이 댓글도 달아주셔서 공식문서를 뒤져봤는데 keep-alive 관련 언급은 전혀 없었다.(내가 못찾은건가.. 꽤 많이 뒤져봤는데......)

그래서 직접 테스트를 진행해보기로했다. 테스트하기위해선 rest template을 사용하는 클라이언트 외에 서버 애플리케이션도 필요하다. 난 테스트를 위해 단순히 header에 keep-alive를 설정하기만하는 샘플 애플리케이션을 서버로 이용했다.

 

val httpClient = HttpClients.custom()
                .setMaxConnTotal(3)
                .setMaxConnPerRoute(3)
                .build()

 

테스트용 설정은 커넥션 풀에 커넥션을 3개 생성해놓도록 설정하고, 100개의 스레드로 rest template을 호출하도록했다.

 

val tp = Executors.newFixedThreadPool(20)

val cd = CountDownLatch(20)

for (i in 0..100) {
  tp.execute {
    apiRepository.call(param, String::class.java)
    cd.countDown()
  }
}

cd.await()
tp.shutdown()

대략 이런식으로 말이다. (참고로 저 apiRepository는 https://multifrontgarden.tistory.com/251 여기서 다룬 repository를 사용한다.)

 

서버에서 keep-alive 미지원

wire shark 를 이용해 패킷을 보는화면이다. 자세하게 올리지도않았고, 자세히 볼필요도 없긴한데 매 호출시 핸드쉐이크가 발생하는 내용이다.(프로토콜에 TCP로 나온부분이 핸드쉐이크 부분이고, HTTP로 표현되는 부분이 실제로 HTTP request/response 부분이다.) 그리고 커넥션 풀을 3개를 설정했지만 실제 사용한 클라이언트 소켓도 3개를 훨씬 넘겨서 사용했다.

 

Caused by: java.net.SocketTimeoutException: connect timed out

 

덤으로 이런 커넥션 타임아웃도 몇개 발생했다.

 

서버에서 keep-alive 지원

이번엔 keep-alive를 지원하는 서버를 호출했을때다. 캡쳐된화면을보면 위에꺼랑 확연하게 다른걸 볼 수 있다. 처음과 끝에서만 핸드쉐이크가 발생하고 keep-alive를 유지하고있기때문에 HTTP 호출로 가득하다.(녹색)

그리고 실제로 이 연결에 사용한 소켓 포트도 내가 설정한 3개만 사용한다. (3개의 소켓 포트로 100번을 다 호출하고있다.)

 

물론 keep-alive 미지원에서 발생했던 커넥션 타임아웃같은건 발생하지않았다. 이 테스트로 인해 클라이언트쪽에서 아무리 커넥션 풀을 만든다해도 서버에서 keep-alive를 지원하지않으면 말짱도로묵이라는걸 알 수있다. 하지만 현재 가장 많이 사용되는 http 1.1의 경우 기본 설정이 keep-alive 지원이며, MSA 형태로 내부 애플리케이션끼리 호출하는 경우엔 서버쪽 애플리케이션도 우리의 영역 안에 있으므로 충분히 효과를 볼 수 있을것이다.

'Java > spring' 카테고리의 다른 글

spring bean lite mode  (0) 2019.08.03
spring boot 2에서 junit 5 사용하기  (6) 2019.05.16
RestTemplate connection pool 설정하기  (7) 2019.05.15
Netflix Hystrix  (4) 2018.12.23
Spring Cloud Config 2  (0) 2018.12.08
Spring Cloud Config 1  (0) 2018.12.07
댓글
  • 프로필사진 java짱 질문이 있습니다.
    DB 커넥션 풀과 같은 경우는 실제 커넥션을 계속 유지한 상태로 pool 에 보관을 하지만
    HTTP는 프로토콜 특성상 stateless 인데 어떻게 커넥션을 연결하고 계속 유지한채로 pool에서 관리하는지 궁금합니다.
    혹, HTTP/1.1의 keep-alive 기능을 이용하게 되는건지요? 만약 그렇다면 연결을 맺어주는 서버측에서 keep-alive 기능을 제공해 줘야 한다는 것인데...
    잘 몰라서 이렇게 질문 올립니다.
    2019.05.22 08:20
  • 프로필사진 LichKing 안녕하세요.
    저도 큰 고민없이 포스팅을 해놨다가 문득 궁금해졌던 내용인데요. 마침 댓글까지 적어주셨네요 ㅎㅎ.

    내용은 본문에 좀 보강하려하고있습니다만 테스트를 진행해본결과 말씀하신대로 서버에서 keep-alive를 지원해줘야합니다.
    (참고로 현재 가장 많이 사용되는 http 1.1에서는 keep-alive 활성화가 디폴트입니다.)
    2019.05.25 21:58 신고
  • 프로필사진 KaSha HTTP는 Stateless이지만, 세션이나 쿠키 등을 이용하여 Stateful하게 사용할수 있습니다. 2019.07.07 02:27
  • 프로필사진 Leo 안녕하세요!

    Caused by: java.net.SocketTimeoutException: connect timed out

    keep-alive 미지원 서버의 경우 위 에러가 덤으로 발생하셨다고 하셨는데, 구체적으로 저 에러가 어떠한 원인 때문에 발생했는지 알려주실 수 있나요?
    저도 개발중에 Connection Pool을 만들어놓지 않은 상태에서 1분주기로 외부 api 콜을 하는 로직을 개발하였었는데, 종종 저 에러가 터지더라구요..

    커넥션 풀을 만들어서 개발하는 방식으로 해결되긴 했습니다만, 아무리 구글링해봐도 정확한 원인 파악이 어려워서 여쭤보게 되었습니다. 매번 커넥션을 맺고 끊는 과정에서 로컬포트가 고갈나서 그런거 같다라는 의견을 듣긴 했습니다만, 로직 상 1분당 1회의 호출밖에 이뤄지지 않기때문에 납득하기 어려운 의견이더라구요. ㅠㅠ
    2020.04.04 17:08
  • 프로필사진 LichKing 포스팅을 작성할 당시에는 connection pool 에 대해서 작성하느라고 connect timeout 에는 별 신경을 쓰지않았었습니다. 더욱이 저는 클라이언트에서 멀티스레드로 빵하고 요청을 날리는 형태라 나머지 스레드들이 커넥션기다리다가 시간초과했나보다 정도로만 생각하고 넘어갔던걸로 기억하는데요.
    호출하시는 외부 api 쪽에 이슈가 있을것같네요.
    2020.04.07 17:18 신고
  • 프로필사진 난너를알어 님 공식 홈페이지 링크 안들어가져요.
    이런 형태로 생성해주면된다. maxConnTotal은 연결을 유지할 최대 숫자이고 maxConnPerRoute는 특정 경로당 최대 숫자이다. 이 외에도 각종 설정들을 제어할 수 있으니 공식 홈페이지를 참고해서 설정을 해주면된다.
    2021.06.01 19:59
  • 프로필사진 LichKing 감사합니다 일단 살아있는걸로 교체했어요 2021.06.02 08:52 신고
댓글쓰기 폼