본문으로 건너뛰기
버전: 5.1.0-11.0

H9. HTTP 클라이언트 타임아웃 미설정으로 Pending(빨강)이 안 사라질 때

Diátaxis: How-to(목표별 가이드) · 대상: 운영자·개발자 ← 목차로

대시보드의 요청 뷰어(Request Viewer)와 실시간 요청 모니터링(Request Velocity)에서 Pending(처리 중) 요청이 빨강으로 계속 남아 줄지 않는다면, 애플리케이션이 외부로 보내는 HTTP 호출에 타임아웃이 설정되지 않았을 가능성이 큽니다. 이 장은 그 원인을 확인하고, HTTP 클라이언트 종류별로 타임아웃을 설정해 해결하는 방법을 다룹니다.

요청 뷰어·실시간 요청 모니터링을 읽는 법은 T2, 위젯의 지표 의미는 R2.1 WAS 차트를 보세요.


증상 — 어디서 보이나

  • 요청 뷰어: 가운데 원형 벽(Wall) 이 줄지 않고 빨강으로 부푼 채 유지됩니다.
  • 실시간 요청 모니터링: Frustrating(빨강) 구간이 쌓이기만 하고 빠지지 않습니다.
  • 트랜잭션이 완료로 집계되지 않고 "처리 중(Pending/Active)" 상태로 오래 남습니다.

평소의 빨강(느린 응답)은 잠시 뒤 처리가 끝나며 사라집니다. 반면 타임아웃이 없으면 호출이 응답을 기다린 채 끝나지 않아 트랜잭션도 끝나지 않으므로 빨강이 계속 남는 점이 다릅니다.


원인 — 타임아웃이 없으면 무한 대기

애플리케이션이 외부 API·내부 서비스로 HTTP 요청을 보낼 때, 상대가 느리거나 응답하지 않으면 타임아웃이 없는 호출은 무한정 응답을 기다립니다. 이때:

  1. 그 요청을 처리하던 WAS 스레드가 점유된 채 반환되지 않습니다.
  2. 트랜잭션이 끝나지 않아 APM은 "처리 중(Pending)" 으로 계속 표시합니다.
  3. 이런 호출이 누적되면 요청 뷰어·실시간 요청 모니터링의 빨강이 사라지지 않고, 결국 스레드 풀까지 고갈될 수 있습니다.

해결의 핵심은 두 종류의 타임아웃을 모두 설정하는 것입니다.

타임아웃의미없으면
연결 타임아웃(connect)TCP 연결 수립까지 대기 한도대상이 응답하지 않을 때 연결 단계에서 무한 대기
응답/읽기 타임아웃(read·response)요청 전송 후 응답(데이터) 대기 한도대상이 응답을 안 주면 무한 대기 (가장 흔한 원인)
(풀 사용 시) 커넥션 획득 타임아웃(connection-request·lease)커넥션 풀에서 빈 연결을 받기까지 대기 한도풀 고갈 시 무한 대기

주의 연결 타임아웃만 설정하고 응답 타임아웃을 빠뜨리는 실수가 가장 흔합니다. 연결은 금방 되지만 응답이 안 오는 경우가 많으므로, 둘 다 설정해야 Pending이 풀립니다.

타임아웃이 없으면 외부 HTTP 호출이 응답을 영원히 기다려 트랜잭션이 끝나지 않고 요청 뷰어의 Pending이 빨강으로 계속 남는다. 연결(connect)과 응답(read) 타임아웃을 설정하면 한도 시점에 실패로 종료되어 Pending이 풀린다

스레드 덤프로 확인 — 응답을 기다리는 스레드 찾기

Pending(빨강)이 정말 "타임아웃 없는 HTTP 호출" 때문인지 스레드 덤프로 확정할 수 있습니다.

뜨는 법 — 좌측 메뉴 ▸ WAS ▸ 트러블슈팅 ▸ 스레드 덤프 분석에서 인스턴스를 고르고 요청합니다. 목록에서 덤프를 고르면 스레드 상세 분석에 각 스레드의 상태와 스택이 보입니다. (자세한 절차·AI 분석은 T3. AI로 원인 분석하기 — 3부 스레드 덤프)

WAS 트러블슈팅 — 스레드 덤프 분석 화면(스레드 목록·상태·상세 스택)

요청을 처리하던 요청 처리 스레드(Tomcat이면 http-nio-…-exec-N)가 RUNNABLE 상태인데도 맨 아래가 socketRead0(소켓 응답 읽기)에 멈춰 있으면, 응답을 기다리고 있는 것입니다. 예:

"http-nio-8080-exec-7" #54 daemon prio=5 tid=0x00007f8a... nid=0x3a2 runnable
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method) ← 여기서 멈춤(응답 대기)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:115)
at java.net.SocketInputStream.read(SocketInputStream.java:168)
at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:735)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1535)
at com.example.PaymentClient.charge(PaymentClient.java:88) ← 우리 코드의 외부 호출
at com.example.OrderService.checkout(OrderService.java:54)
at com.example.OrderController.order(OrderController.java:37)
... (서블릿 필터 · 디스패처)

읽는 법

  • socketRead0(또는 read) 에서 멈춘 RUNNABLE 스레드 = 응답을 read 타임아웃 없이 기다리는 중. (CPU를 쓰지 않지만 상태는 RUNNABLE로 보입니다 — 네트워크 읽기의 특성.)
  • 그 위의 우리 코드 프레임(PaymentClient.chargeOrderController.order)이 어떤 호출이 응답을 못 받고 있는지대상을 알려 줍니다.
  • 한 번만 떠도 그 스레드의 '경과시간' 으로 무한 대기를 판단할 수 있습니다. 스레드 덤프 목록·상세에 표시되는 경과시간이 평소 정상 처리 시간보다 비정상적으로 길면(예: 평소 수백 ms인데 수십 초~분 단위로 계속 증가) socketRead0에 멈춘 그 스레드는 무한 대기 상태입니다. 이 스레드들이 곧 요청 뷰어·실시간 요청 모니터링의 Pending(빨강) 입니다.

참고 클라이언트마다 윗부분 스택은 다르지만 바닥은 소켓 읽기로 같습니다. 예: Apache HttpClient …SessionInputBufferImpl.streamRead · OkHttp okio.…read · 모두 끝은 socketRead0. 비동기/NIO 방식(java.net.http.HttpClient 등)은 호출 스레드가 CompletableFuture.get/셀렉터 대기로 보일 수 있으나, 응답 타임아웃이 없으면 무한 대기인 점은 같습니다.

참고 상단 툴바의 CogentAI 분석 버튼을 누르면 AI가 공통 스택·오래 걸리는 호출 대상을 추려 줍니다(T3 7단계). 타임아웃 설정 에 같은 덤프를 뜨면, 대기하던 스레드가 한도 시점에 SocketTimeoutException으로 끝나 더는 보이지 않습니다.


해결 — HTTP 클라이언트별 타임아웃 설정

각 단위는 밀리초(ms) 또는 Duration입니다. 아래 값은 예시이며, 실제 값은 권장 값을 참고해 환경에 맞게 조정하세요.

1) HttpURLConnection (JDK 내장 · java.net)

기본값이 둘 다 0(무한 대기) 이므로 반드시 명시해야 합니다.

URL url = new URL("https://api.example.com/data");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000); // 연결 타임아웃 (ms), 0 = 무한(기본)
conn.setReadTimeout(5000); // 응답(read) 타임아웃 (ms), 0 = 무한(기본)

2) java.net.http.HttpClient (Java 11+)

연결 타임아웃은 클라이언트에, 응답 타임아웃은 요청에 설정합니다. 둘 다 미설정 시 무한입니다.

HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3)) // 연결 타임아웃
.build();

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.timeout(Duration.ofSeconds(5)) // 응답 타임아웃 (요청 단위)
.build();

응답 타임아웃을 초과하면 HttpTimeoutException이 발생합니다. 요청마다 timeout(...) 을 지정해야 하므로, 공통 요청 빌더나 래퍼에서 빠뜨리지 않도록 합니다.

3) Apache HttpClient (HttpComponents)

풀 기반이라 커넥션 획득 타임아웃까지 함께 설정하는 것을 권장합니다.

4.x (가장 널리 쓰임) — 단위는 ms(int):

RequestConfig config = RequestConfig.custom()
.setConnectTimeout(3000) // 연결 타임아웃
.setSocketTimeout(5000) // 응답(소켓 read) 타임아웃
.setConnectionRequestTimeout(2000) // 풀에서 커넥션 획득 대기 타임아웃
.build();

CloseableHttpClient client = HttpClients.custom()
.setDefaultRequestConfig(config)
.build();

5.xTimeout 타입 사용. 연결·소켓 타임아웃은 커넥션 매니저의 ConnectionConfig 로, 응답 타임아웃·커넥션 획득은 RequestConfig 로 분리되었습니다:

ConnectionConfig connConfig = ConnectionConfig.custom()
.setConnectTimeout(Timeout.ofSeconds(3)) // 연결 타임아웃
.setSocketTimeout(Timeout.ofSeconds(5)) // 소켓(read) 타임아웃
.build();

PoolingHttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()
.setDefaultConnectionConfig(connConfig)
.build();

RequestConfig reqConfig = RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofSeconds(2)) // 풀에서 커넥션 획득
.setResponseTimeout(Timeout.ofSeconds(5)) // 응답 타임아웃
.build();

CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(cm)
.setDefaultRequestConfig(reqConfig)
.build();

4) OkHttp (Square)

OkHttp는 connect·read·write 기본값이 각 10초(무한 아님)지만, 전체 호출 타임아웃 (callTimeout)은 기본 0(무한) 입니다. 리다이렉트·인터셉터·재시도가 누적되면 개별 단계가 10초여도 전체는 무한정 길어질 수 있으므로 callTimeout까지 지정하는 것을 권장합니다.

OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(3, TimeUnit.SECONDS) // 연결
.readTimeout(5, TimeUnit.SECONDS) // 응답(read)
.writeTimeout(5, TimeUnit.SECONDS) // 요청 본문 전송(write)
.callTimeout(10, TimeUnit.SECONDS) // 전체 호출 한도 (권장)
.build();

5) Spring RestTemplate

RestTemplate 자체에는 타임아웃 setter가 없습니다. 요청 팩토리(ClientHttpRequestFactory) 에 설정한 뒤 RestTemplate주입해야 합니다.

SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(3000); // 연결 타임아웃 (ms)
factory.setReadTimeout(5000); // 응답 타임아웃 (ms)

RestTemplate restTemplate = new RestTemplate(factory); // 팩토리를 반드시 주입

Spring Boot에서는 RestTemplateBuilder로 더 간단히 설정합니다:

@Bean
RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofSeconds(3))
.setReadTimeout(Duration.ofSeconds(5))
.build();
}

참고 커넥션 풀과 커넥션 획득 타임아웃까지 제어하려면 요청 팩토리로 HttpComponentsClientHttpRequestFactory(Apache HttpClient 기반)를 써서 위 3)의 connectionRequestTimeout을 함께 설정합니다. 최신 Spring(6/Boot 3)의 RestClient·WebClient도 같은 원리로 connect/response 타임아웃을 반드시 지정하세요.


권장 값 가이드

절대 정답은 없으며 대상 서비스의 정상 응답시간을 기준으로 잡습니다.

타임아웃시작값(예)정하는 기준
연결(connect)1~3초같은 네트워크면 짧게(1초), 외부망이면 2~3초
응답·read5~10초대상의 P99 응답시간 + 여유. 너무 짧으면 정상 요청도 끊김
커넥션 획득(lease)1~2초풀이 고갈됐을 때 빨리 실패시켜 무한 대기 방지

참고 대상의 P99 응답시간은 요청 뷰어·응답시간 차트(R2.1)로 확인할 수 있습니다. read 타임아웃은 이 값보다 충분히 크게 잡되, 무한이 아니게만 하면 Pending 누적은 막힙니다.


검증 — Pending이 풀리는지 확인

  1. 타임아웃을 설정·배포한 뒤, 대시보드의 요청 뷰어실시간 요청 모니터링을 다시 봅니다.
  2. 대상이 느리거나 죽어 있어도, 호출이 타임아웃 시점에 실패로 끝나 트랜잭션이 종료됩니다. → 빨강이 타임아웃 한도 내에서 사라지고, Pending이 무한정 쌓이지 않습니다.
  3. 실패한 호출은 오류로 집계되므로, 이제는 H6느린/실패한 외부 호출 자체(대상 서비스의 지연·장애)를 추적합니다.

타임아웃은 무한 대기(Pending)를 멈추는 안전장치일 뿐, 대상이 느린 근본 원인을 고치는 것은 아닙니다. 빨강이 멈춘 뒤에는 왜 느린지(대상 API·네트워크·DB)를 별도로 봐야 합니다.


잘 안될 때

증상점검
타임아웃을 넣었는데도 Pending 빨강 지속두 타임아웃 중 하나만 넣지 않았는지 확인 — 보통 연결만 하고 응답(read) 타임아웃을 빠뜨림
풀 기반 클라이언트인데 가끔 무한 대기커넥션 획득(connection-request·lease) 타임아웃 미설정 — 풀 고갈 시 무한 대기
OkHttp인데 간헐적 무한 대기callTimeout(기본 0=무한) 미설정 — read만으로는 리다이렉트·인터셉터 누적을 못 막음
RestTemplate에 setter만 호출했는데 무한요청 팩토리를 new RestTemplate(factory)로 주입했는지 확인 — setter만 호출하고 미주입이 흔함
타임아웃 후 정상 요청까지 끊김read 타임아웃이 너무 짧음 — 대상 P99 응답시간 + 여유로 상향
코드가 여러 곳에 흩어져 일부만 설정됨공통 클라이언트 빈/팩토리로 한 곳에서 타임아웃을 강제 — 호출부마다 누락 방지

관련 문서