Skip to content

[Study] Java Http 다운로드 라이브러리 조사 및 다운로드 방식에 따른 성능테스트

Bear edited this page Apr 27, 2023 · 12 revisions

다운로드 라이브러리 조사

Java HTTP Client

참고 : https://www.wiremock.io/post/java-http-client-comparison

우선 서드파티 라이브러리를 제외하고, 자바가 제공해주는 HTTP 클라이언트를 비교 분석하고자하였다.

HttpURLConnection

JDK 1.1 부터 존재한 클라이언트로 아직도 많이 사용되는 클라이언트라고 볼 수 있다.

내부 동작

  • URL 객체의 openConnection() 메서드를 통해서 커넥션을 맺는다.
    • 내부적으로는 기본적으로 URLStreamHandler를 사용하여 HTTP 혹은 HTTPS에 따라서 invoke가 수행된다.
// java.net.URL
public URLConnection openConnection() throws java.io.IOException {
        return handler.openConnection(this);
}


// sun.net.www.protocol.https.Handler (HTTP일 경우 다른 핸들러를 사용한다)
protected java.net.URLConnection openConnection(URL u, Proxy p)
        throws IOException {
        return new HttpsURLConnectionImpl(u, p, this); // 이 메서드를 호출하여 java.net.URLConnection을 리턴한다. 
}
  • 이후 전달받은 HttpURLConnection에서 요청된 URL의 데이터를 읽어오기 위해서 getInputStream() 메서드를 호출한다.
    • 이때, chunk-lenght, fixed-length 등이 존재한다면 스트리밍 모드로 동작한다.
// sun.net.www.protocol.http.HttpURLConnection
  @Override
    public synchronized InputStream getInputStream() throws IOException {
        connecting = true;
        SocketPermission p = URLtoSocketPermission(this.url);

        if (p != null) {
            try {
                return AccessController.doPrivilegedWithCombiner(
                    new PrivilegedExceptionAction<InputStream>() {
                        public InputStream run() throws IOException {
                            return getInputStream0();
                        }
                    }, null, p
                );
            } catch (PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        } else {
            return getInputStream0(); // 별도의 SocketPermission이 부여안되어있다면 이 메서드를 통해서 스트림을 가져온다. 
        }
    }


    private synchronized InputStream getInputStream0() throws IOException {
      ... (중략) ...
    }

이렇게 커넥션을 가져오면서 처리된다고 볼 수 있다.

HttpURLConnection을 통한 다운로드 테스트

  1. 더미데이터 파일 크기 : 800MB
  2. 네트워크 지연을 최소화하기 위해서 CloudFront (No-Cache) 요청으로 수행
  • 병렬 처리 알고리즘
    • 메인쓰레드에서 InputStream을 100메가씩 버퍼로 읽어들임.
    • 후에 executor.excute() 메서드를 통해서 쓰레드에 던져서 병렬 처리하도록 작업 (쓰레드 내부에서는 50메가의 버퍼를 사용)
테스트 변인 설정 값 비고
GC G1GC 가비지컬렉터 알고리즘
XMS 2g 최소 힙 영역
XMX 2g 최대 힙 영역
CPU Core 10 M1 Macbook Pro(H/W)
RAM 32G M1 Macbook Pro(H/W)
쓰레드 수 평균 다운로드 시간(S) 평균 다운로드 시간(MS)
3 약 52초 52874
4 약 40초 40764.66666667
6 약 32초 32649.66666667
8 약 31초 31512.33333333
10 약 29초 29888.33333333

CPU 코어 수와 같은 쓰레드 수인 10개를 활용할 때 제일 약 29초 로 제일 빨랐던 점을 확인할 수 있다.

HttpClient

JDK11 이후부터 추가된 클라이언트로 내부적으로 셀렉터로 구현되어있다.

Screenshot 2023-04-26 at 22 12 06

즉, 이 클라이언트는 NIO 방식으로 구현되어있다고 보면된다. Selector를 사용함으로써 멀티쓰레드가 아니여도 쓰레드 1개만으로도 논블로킹 처리를 하면서 빠르게 설정이 가능하다.

Screenshot 2023-04-26 at 22 22 53

HttpClient를 통한 다운로드 테스트

  • 동기 다운로드 시
버퍼 크기 평균 다운로드 시간(S) 평균 다운로드 시간(MS)
0 약 30초 30994
10MB 약 25초 25263.33333333
50MB 약 17초 17434.66666667
100MB 약 31초 31605.33333333

왜? 버퍼 크기를 늘렸음에도 평균 다운로드 시간을 줄어들게 되었는가? 이 부분은 고민할 껀덕지가 많으나 현재 아래와 같은 가설을 세우게 되었다.

네트워크 속도의 영향으로 큰 버퍼의 사이즈만큼 읽기를 위해서 대기하는 것보다 작은 크기의 버퍼를 채우는 속도가 빠르고 그만큼 I/O가 이뤄질 수 있어서지않을까?

이 부분에 대한 가설은 나중에 검증해보도록한다.

  • 비동기 다운로드 시
버퍼 크기 평균 다운로드 시간(S) 평균 다운로드 시간(MS)
10MB 약 15초 15014
50MB 약 14초 14911.33333333
100MB 약 15초 15004.66666667

HttpClent.sendAsync() 메서드를 활용하여 테스트를 하였을 경우에는 버퍼 크기에 유의미한 차이를 나타나지는 않았다. (버퍼 크기가 0일때 제외하고) 유의미한 점은 Async로 처리하는 방식이 Sync로 처리하는 부분보다 빠르다는 점이다. 아마도 Non-Blocking이기 때문에 좀 더 빠르다고 추측을 하고 있다.