MSA(Microservice Architecture) 환경에서는 서비스 간 통신이 매우 중요하다. Spring Cloud에서 제공하는 Feign 클라이언트는 선언적 방식으로 HTTP API 통신을 쉽게 구현할 수 있게 해주는 도구이다.
Feign의 기본 HTTP 클라이언트 구현체인 HttpURLConnection은 별도의 의존성 없이 사용할 수 있다는 장점이 있지만, 동시에 많은 요청이 발생하는 프로덕션 환경에서는 성능상의 한계를 보일 수 있다. 이에 대한 대안으로 Apache HttpComponents 5를 HTTP 클라이언트로 사용하는 방법을 살펴보고, 실제 성능 차이를 비교 분석해보자.
구분 | Java HttpURLConnection | Apache HttpComponents 5( DEFAULT ) 1. max-connections-per-route: 2 2. max-connections: 20 | Apache HttpComponents 5 1. max-connections-per-route: 100 2. max-connections: 100 |
---|---|---|---|
http_req_duration | 394.44ms, 20.85s, 65.61ms, 25.18ms, 237.75ms (평균) | 41.92ms, 353.00ms, 32.53ms, 11.34ms, 62.89ms (평균) | 31.74ms, 378.38ms, 24.71ms, 8.81ms, 47.05ms (평균) |
응답 시간의 변동성 | 응답 시간의 변동성이 매우 큼 (20ms에서 394ms까지 큰 편차) | 첫 번째보다는 안정적이지만 여전히 편차 존재 (11ms에서 353ms) | 전반적으로 가장 안정적인 패턴 |
그래프 양상 | 그래프가 매우 불안정하며 스파이크가 자주 발생 | Blocked time이 16.18ms로 connection pool 대기 시간 발생 | Blocked time이 50.67ms로 증가했지만, 전체적인 처리량은 더 안정적 |
![]() ![]() | ![]() ![]() | ![]() ![]() |
구분 | Java HttpURLConnection | Apache HttpComponents 5 1. max-connections-per-route: 100 2. max-connections: 100 |
---|---|---|
CPU 사용률 | CPU 스파이크가 높고(최대 50-60%), 변동이 심함 | CPU 사용이 더 안정적(약 20% 수준 유지)하고 예측 가능한 패턴 |
Heap 메모리 | 크기: 1,358,954,528 B (약 1.3GB) GC 활동이 더 빈번함 | 크기: 415,236,128 B (약 415MB) GC 활동이 덜 빈번함 |
스레드 | Live: 26 (peak: 204) Daemon: 23 | Live: 47 (peak: 140) Daemon: 42 |
![]() | ![]() |
public abstract class HttpURLConnection extends URLConnection {
// 연결 상태를 나타내는 boolean 값
private boolean connected = false;
// 기본 타임아웃 값은 무한대(-1)
private int connectTimeout = -1;
private int readTimeout = -1;
// chunked streaming mode 설정
protected int chunkLength = -1;
// HTTP 메소드
private String method = "GET";
public void connect() throws IOException {
if (connected) {
return;
}
// 실제 연결 로직
// 연결이 성공하면 connected = true
connected = true;
}
// 연결을 재사용할 수 없음
public void disconnect() {
// 연결 해제
connected = false;
}
// 응답 코드 및 헤더 검증
public int getResponseCode() throws IOException {
// 응답을 받기 위해 connect()를 호출
if (!connected) {
connect();
}
// 응답 코드 반환
return responseCode;
}
}
fyi; HttpURLConnection
// 매 요청마다 새로운 연결 생성
protected java.net.URLConnection openConnection(URL u, Proxy p)
throws IOException {
return new HttpURLConnection(u, p, this);
}
fyi; Handler
System.setProperty("http.keepAlive", "true");
System.setProperty("http.maxConnections", "5");
public class KeepAliveCache
extends HashMap<KeepAliveKey, ClientVector>
implements Runnable {
// 전역 락으로 인한 병목 현상
private final ReentrantLock cacheLock = new ReentrantLock();
// 중첩된 락킹으로 인한 성능 저하
public void remove(HttpClient h, Object obj) {
cacheLock.lock(); // 첫 번째 락
try {
ClientVector v = super.get(key);
if (v != null) {
v.lock(); // 두 번째 락
try {
v.remove(h);
} finally {
v.unlock();
}
}
} finally {
cacheLock.unlock();
}
}
fyi; KeepAliveCache
Java 17 의 Lock 개선
Java 17에서는 virtual thread 도입을 앞두고 HTTP 프로토콜 핸들러의 동시성 제어를 개선했다. JDK-8229867 이슈를 통해 기존의 synchronized 키워드 기반 동기화를 ReentrantLock으로 전환하고, 불필요한 동기화를 제거하는 등의 변경이 이루어졌다.
하지만 이러한 개선에도 여전히 한계는 존재한다. 단일 전역 락을 사용하는 중앙 집중식 락킹 구조와 중첩된 락킹으로 인한 데드락 가능성, 그리고 락 획득/해제의 오버헤드는 여전히 남아있다.
https://mail.openjdk.org/pipermail/net-dev/2020-October/014584.html
https://github.com/openjdk/jdk/pull/558/files
TCP 3-way Handshake: 2-5ms
SSL/TLS Handshake (HTTPS): 5-15ms
총 연결 시간: 7-20ms/요청
// 각 연결마다 새로운 버퍼 할당
byte[] buffer = new byte[8192]; // 기본 버퍼 크기
public class PoolingHttpClientConnectionManager {
private final CPool pool;
// 풀 설정
public void setMaxTotal(final int max) {
this.pool.setMaxTotal(max);
}
// 커넥션 임대
@Override
public ConnectionRequest requestConnection(HttpRoute route, Object state) {
final Future<CPoolEntry> future = pool.lease(route, state, null);
return new ConnectionRequest() {
@Override
public HttpClientConnection get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException {
final CPoolEntry entry = future.get(timeout, unit);
return entry.getConnection();
}
};
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttp5Client.class)
@ConditionalOnMissingBean(org.apache.hc.client5.http.impl.classic.CloseableHttpClient.class)
@ConditionalOnProperty(value = "spring.cloud.openfeign.httpclient.hc5.enabled", havingValue = "true",
matchIfMissing = true)
@Import(org.springframework.cloud.openfeign.clientconfig.HttpClient5FeignConfiguration.class)
protected static class HttpClient5FeignConfiguration {
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(org.apache.hc.client5.http.impl.classic.CloseableHttpClient httpClient5) {
return new ApacheHttp5Client(httpClient5);
}
}
Spring Cloud 2022.0.0 (Spring Boot 3.0.x에 대응) 기준으로 implementation("io.github.openfeign:feign-hc5")
만 추가하면 Apache HttpClient 5를 Feign의 HTTP 클라이언트로 사용할 수 있으며, 설정 없이 자동으로 적용된다.
이전 버전에서는 아래 설정이 추가로 필요하다.
spring:
cloud:
openfeign:
httpclient: true
Spring Boot 버전별 Spring Cloud 의존성은 이 페이지에서 확인할 수 있다.