탈 자바 8과 스프링부트 3.0을 위한 NIO HttpClient를 찾아서

🧐 | 2023-03-22

자바 NIO HttpClient 비교

시작하며

안녕하세요, 넷마블 플랫폼개발실 플랫폼개발2팀 최승관입니다.

최근 스프링(Spring) 6.0 버전스프링부트(Spring Boot) 3.0 버전이 정식 출시했습니다. 이들 버전 업데이트 내용 가장 첫 줄에는 자바(JAVA) 최소 지원 버전이 17로 올랐다는 설명이 나와 있더군요. 이제 자바 8 버전 탈출이 더 가속화될 것 같습니다.

플랫폼개발실에서는 자바 17 버전으로 업그레이드를 준비하는 과정에서, 자바 11 버전에서 정식으로 추가됐던 HttpClient(JEP110) 도입 영향을 살펴봐야 했습니다. 자주 사용하는 HttpClient와 간단히 비교한 결과를 공유합니다.

JEP 110: HTTP/2 Client (Incubator)에 나온 HTTP/1.1의 성능 요구 사항(Goal)

– 기존 `HttpURLConnection` 구현과 동등한 성능이 나와야 합니다.

– Apache HttpClient 라이브러리, Netty, Jetty를 클라이언트 API로 사용할 때와 동등한 성능이 나와야 합니다.

– 새로운 API의 메모리 사용량은 `HttpURLConnection`, Apache HttpClient, Netty, Jetty를 클라이언트 API로 사용할 때 대비 같거나 낮아야 합니다.

테스트 계획 세우기

먼저, 테스트를 위한 환경, 항목, 시나리오를 정의하고 목표를 세웠습니다. 동일한 조건 아래에서 동작하는 각 HttpClient들의 안정성과 처리 속도를 비교한 근거자료를 활용하면, 신규 프로젝트 상황에 맞는 선택을 할 수 있습니다.

테스트 목표

NIO(New Input Output)를 지원하는 HttpClient들의 안정성과 처리 속도를 비교해 상황에 맞는 HttpClient 선택 판단 근거를 마련합니다.

테스트 항목

CPU 사용률, 메모리 사용량, 처리 속도를 측정합니다.

테스트 시나리오

각 HttpClient를 동일한 환경에서 Mock 서버(±8ms 응답)에 1만 번 일괄 요청을 일정 간격으로 100번 시도합니다. 단, 1회 시도마다 전체 응답 완료까지 걸린 시간을 측정하기 위해 동기식 논블로킹 IO(Sync Non-Blocking IO) 방식을 적용합니다.

테스트 환경

OpenJDK 17 버전과 스프링부트 3.0.2 버전을 설치한 장비와 옵션 모두 동일하게 세팅했습니다.

HttpClient 선택

다양한 HttpClient가 있지만, 스프링 WebClient에서 지원하거나 자주 사용하는 라이브러리를 선정했습니다.

  • OpenJDK
  • Unirest
  • Apache HttpComponents Client
  • Netty
  • Jetty

또한 동일한 라이브러리라도 스프링 WebClient를 사용할 경우 의미 있는 차이가 있는지도 같이 점검하기 위해 OpenJDK와 Apache Httpcomponents를 추가 테스트 대상으로 선정했습니다. 아래는 테스트 대상으로 선택한 각 라이브러리의 특징과 설정을 정리한 내용입니다.

OpenJDK

  • 버전: 17

Config

  • thread count = 3
  • connectionTimeout = 1,000ms
  • httpRequestTimeout = 2,000ms

특징

  • 별도 라이브러리를 사용하지 않아도 되므로, 의존성 관계에서 자유로움.
  • headerquerystring을 사용하는 경우, 개발 효율이 떨어짐.
  • retry 기능이 없어서 별도 구현 필요.
  • 자바 11 버전 이상 지원

Unirest

  • 버전: 3.14.1 (가이드), Apache httpcompnents를 사용(ver 4.5.x)

Config

  • concurrency max total = 40, max per route = 40
  • connectionTimeout = 1,000ms
  • socketTimeout = 60,000ms (default)
  • Config 옵션 설명

특징

  • 개발이 상대적으로 쉬움. 공통화가 잘 돼 있음.
  • 라우트(route) 단위로 동시성 관리.
  • http keepalive 설정 옵션이 별도 존재하지 않음.
  • JSON 형태 오브젝트 변환 기능을 기본으로 제공함.

Apache httpcomponents client5

Config

  • IoThreadCount = 3
  • SoTimeout = 60s
  • concurrency max total = 40, max per route = 40

특징

  • 다양한 기능과 설정 제공.
  • 라우트(route) 단위로 스레드 관리.
  • keepalive 설정 커스터마이징 가능.
  • IoThreadCount 늘려도 성능 효과 없음. max per route를 늘려야 함.
  • nGrinder, Jenkins 지원.

Netty + Spring WebClient

Config

  • connectionTimeout = 1,000ms
  • responseTimeout = 2,000ms
  • Socket Read/Write timeout = 60s
  • keepalive = true

특징

  • Micrometer 연동 가능

Jetty + Spring WebClient

Config

  • connectionTimeout = 1,000ms
  • MaxConnectionsPerDestination = 40

특징

  • maxRequestsQueuedPerDestination로 최대 요청 queue size를 설정 가능.
  • setAddressResolutionTimeout으로 DNS lookup 타임아웃(timeout) 설정 가능.
  • request 생성 시점에 타임아웃(timeout) 설정 가능.

WebClient는 성능에 영향을 주는가

먼저 WebClient가 중간에서 리소스를 어느 정도 사용하는지 확인해 봤습니다.

Apache vs Apache&WebClient

Apache를 사용할 경우에는 `httpclient5`만 있으면 되지만, Apache&WebClient를 사용하기 위해서는 httpcore5-reactive를 의존(dependency) 요소로 추가해야만 합니다. CPU 사용량은 둘 다 대략 ±40%이고, 메모리는 오히려 Apache가 최대 사용량이 더 많은 것을 확인할 수 있었습니다.

하지만 대체로 처리시간은 Apache가 200ms 정도로 빨랐습니다. 둘 다 같은 Apache HttpClient를 사용했지만, 동작 방식에서 차이가 있기 때문에 OpenJDK로 다시 테스트를 진행했습니다.

OpenJDK vs OpenJDK&WebClient

OpenJDK로 확인한 결과 CPU 사용률, 메모리 사용량, 처리시간 모두 거의 동일한 결과가 나왔습니다. 즉, WebClient는 성능에 거의 영향을 주지 않는 것을 확인했습니다.

본격적인 비교 진행

이제 Netty, Jetty, Unirest를 살펴보도록 하겠습니다.

Netty

Netty는 Apache보다 처리시간이 100ms정도 느리고 CPU 사용률은 60% 정도로, 더 많이 사용하는 것을 확인할 수 있었습니다.

Jetty

Jetty는 Netty와 마찬가지로 CPU 사용률이 높은 편에 속했지만, 처리 속도는 OpenJDK와 비슷하거나 조금 더 빠른 정도였습니다.

Unirest

마지막으로 Unirest는 Apache 기반이지만 4.x 버전이라 그런지는 몰라도, Jetty나 Netty와 유사하게 CPU 사용률이 높은 편이었고 처리시간은 Jetty와 많이 유사했습니다.

결론

테스트 결과 수치를 보면, 블로킹(Blocking)과 논블로킹(Non-Blocking)의 차이만큼 의미 있는 성능 차이는 볼 수 없었습니다. 서로 미세한 차이를 가진 만큼, 사용 목적이나 환경에 맞춰 적합하게 선택지를 고르면 될 것으로 보입니다.

빠른 처리가 중요한 서비스라면

빠른 처리가 중요한 서비스라면, 낮은 CPU 사용률이 나온 “Apache Httpcomponents Client5”를 쓰는 것이 조금이라도 더 나은 효율을 얻을 선택이 될 것입니다.

모듈(module) 형태로 배포하는 경우라면

모듈(module) 형태로 배포하는 경우라면, 라이브러리끼리 충돌 가능성이 없는 “OpenJDK HttpClient”가 좋은 선택으로 보입니다.

스프링을 사용하는 환경이라면

스프링을 사용하는 환경이라면, `WebClient&Apache`나 `WebClient&OpenJDK` 조합을 사용해 리소스 사용은 줄이면서 유지보수 편의성을 높일 수 있습니다.

마치며

이 글에 작성하진 않았지만, 개발 생산성 측면에서는 단연 돋보이는 스프링 클라우드 오픈페인(Spring Cloud OpenFeign)도 테스트해 봤습니다. 오픈페인은 아직 블로킹 IO(Blocking IO) 방식만 지원하고 있고 성능이 많이 낮아서, 이번 테스트 대상이었던 NIO HttpClient들과는 비교하기 힘들었습니다. 다만 오픈페인에서도 NIO를 준비하고 있는 만큼, 나중에는 스프링을 사용하는 곳에서 많이 사용하게 되지 않을까 조심스레 예상해봅니다.

언제나 그렇듯 “절대”라는 것은 없다고 생각합니다. 각 환경마다 미세한 차이로 성공과 실패가 나뉘기도 하니까요. HTTPS, 파일 업로드, 설정, 버전, 요청과 응답 크기 등 여러 조건을 바꿔서 테스트하면 다른 결과가 나올 수 있습니다.

이번 테스트 환경과 조건에서는 서로 큰 차이가 나오진 않았습니다. 하지만 테스트 조건에 따라서 결과는 항상 바뀔 수 있음을 꼭 인지해주시길 당부드립니다.

비슷한 문제를 해결하려는 분들께 작게나마 도움이 됐으면 합니다.