티스토리 뷰
이전글에서 timeout 에 대한 설명을 적어놨었다. 이후 실제로 문서에 나와있는대로 동작하는지 확인해보기로 했다. 해당글에서 정리한 내용이 정확하다면 response timeout 과 socket timeout 을 훨씬 초과하여 응답이 오더라도 바이트들만 제 시간 내에 도착한다면 예외없이 요청이 성공해야한다.
테스트에 사용한 apache httpcomponents client 의 버전은 5.5이다.
org.apache.httpcomponents.client5:httpclient5:5.5
# 코드준비
먼저 stream 으로 응답을 하는 간단한 API 하나를 구성했다.
@GetMapping(value = "/stream", produces = MediaType.TEXT_PLAIN_VALUE)
public StreamingResponseBody stream() {
return outputStream -> {
for (int i = 0; i < 50; i++) {
String data = "data " + i + "\n";
outputStream.write(data.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
try {
Thread.sleep(100); // 100ms 간격
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}해당 API 는 "data 0, data 1, data 2 ..." 형태로 응답을 스트리밍으로 하게된다. 그리고 스트리밍 간격은 100ms 이다. 로컬에서 서버를 구동시켜서 curl 로 실행해보면 응답을 확인해볼 수 있다.
curl http://localhost:8080/stream
해당 API 를 호출할 클라이언트 코드를 작성한다.
public static void main(String[] args) throws Exception {
ConnectionConfig connectionConfig = ConnectionConfig.custom()
.setConnectTimeout(50, TimeUnit.MILLISECONDS)
.setSocketTimeout(100, TimeUnit.MILLISECONDS) // socket timeout 100ms
.build();
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
poolingHttpClientConnectionManager.setDefaultConnectionConfig(connectionConfig);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(200, TimeUnit.MILLISECONDS)
.setResponseTimeout(100, TimeUnit.MILLISECONDS) // response timeout 100ms
.build();
CloseableHttpClient httpClients = HttpClients.custom()
.setConnectionManager(poolingHttpClientConnectionManager)
.setDefaultRequestConfig(requestConfig)
.build();
HttpGet httpGet = new HttpGet("http://localhost:8080/stream");
httpClients.execute(httpGet, (classicHttpResponse) -> {
HttpEntity entity = classicHttpResponse.getEntity();
try (InputStream in = entity.getContent();
BufferedReader reader = new BufferedReader(
new InputStreamReader(in, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
return null;
});
}timeout 4개를 모두 설정하고 있지만 주석으로 표시한 socketTimeout, responseTimeout 이 핵심이니 이 부분만 변경해주며 테스트하면 된다.
# 테스트
## socketTimeout 150ms responseTimeout 150ms

첫번째 바이트를 대기하는 responseTimeout 도 안정권이고, 바이트 간격을 대기하는 socketTimeout 도 안정권이다. 성공적으로 응답을 받고, 차례대로 출력하는걸 볼 수 있다.
## socketTimeout 150ms responseTimeout 80ms
첫번째 바이트를 대기하는 시간이 80ms 로 줄었다. 하지만 위 예제 API 는 첫번째 바이트는 sleep 없이 바로 응답하고, 두번째 바이트부터 sleep 을 걸고있기 때문에 timeout 이 문서대로 동작한다면 요청은 성공해야한다.

하지만 요청이 실패했다. 그런데 자세히보면 첫번째 바이트인 "data 0" 은 정상출력한걸 볼 수 있다. 즉 두번째 바이트를 대기하다가 예외가 발생한 것이다.
## socketTimeout 80ms responseTimeout 150ms
이번엔 두 타임아웃 값을 반대로 변경했다. 예상대로 동작한다면 위에서 발생한 예외가 위가 아니라 여기서 발생해야한다.

오잉 성공했다.
## socketTimeout 80ms responseTimeout 80ms
예상대로 동작한다면 첫번째 바이트만 출력되고, 예외가 발생해아한다.

예상대로 동작한다.
# 문제
socketTimeout 과 responseTimeout 이 모두 여유롭거나 모두 부족할땐 예상대로 동작했는데 두 값이 한쪽만 부족할땐 예상과 다르게 동작하고 있다. 원인이 무엇일까?
apache httpcomponents client 는 제대로된 문서가 없고, javadoc 이 문서를 대체하고있어 일단 공식적인 문서자료를 확인하기가 매우 어려웠다. 결국은 코드를 들여다볼 수 밖에 없었는데 코드에서 원인을 찾을 수 있었다.

먼저 직접적인 HTTP Connection 을 생성하는 PoolingHttpClientConnectionManager 클래스를 확인해보면 이렇게 connection 생성시 socketTimeout 을 설정하고 있다. 그런데 코드를 더 들어가보면

실제로 요청을 실행하는 시점에 responseTimeout 을 꺼내고, socketTimeout 에 설정하고 있다.
이걸 확인하는 순간 responseTimeout 이 발생하는 시점에도 ResponseTimeoutException 이 아니라 SocketTimeoutException 이 발생하는 이유까지 밝혀졌다. responseTimeout 이 socketTimeout 을 덮어쓰고 있는 것이다.
즉 문서에서 명시하고 있는대로 첫번째 응답 바이트까지 responseTimeout 이 관여하고, 두번째 바이트부터 socketTimeout 이 관여하는게 아니다. 사실 이전에 timeout 정리를 할때 개인적으로 느꼈던 부분은 responseTimeout 과 socketTimeout 을 이 정도로 분리해서 관리하면 코드 작성하기가 번거롭겠다 라고 생각했었는데, 그저 그냥 덮어쓰고 있던 것이다.
그리고 socketTimeout 은 ConnectionConfig 에서, responseTimeout 은 RequestConfig 라는 서로 다른 Config 객체를 통해 설정하고 있던것도 문맥을 이해하게 되었다. socketTimeout 은 connection 생성시 전달하고, responseTimeout 은 실제 요청(request)시 전달하기 때문이었던 것이다. 이 때문에 connection 수준에서는 socketTimeout 이 적용되고, 요청 수준에서는 responseTimeout 이 적용된다. responseTimeout 을 설정했다면 socketTimeout 은 적용되지 않는다!!!
# 마무리
이런 내용은 공식문서는 물론이고, 웹 검색에서도 찾기가 쉽지 않았다. 하지만 apache httpcomponents 를 사용하는 입장에서는 매우 중요한 요소이다. 생각보다 문서화가 상당히 부실하여 실망스럽기도 했지만 문서 변경에 대한 PR( https://github.com/apache/httpcomponents-client/pull/712 ) 을 제출했다. 받아들여질지는 모르겠지만.
'Java' 카테고리의 다른 글
| apache httpcomponents client 사용시 상황별 timeout과 connection pool 동작 (0) | 2025.08.15 |
|---|---|
| synchronized 구문에서 virtual thread 동작과 JEP 491 (0) | 2025.08.10 |
| JSR354 JavaMoney (1) | 2025.04.25 |
| 예외처리는 어떻게 하는게 좋을까 (3) | 2025.01.05 |
| 올바른 추상화와 인터페이스 설계 (1) | 2024.12.22 |
- Total
- Today
- Yesterday
- java
- Git
- programming
- 정규표현식
- mariadb
- generics
- EffectiveJava
- clean code
- spring cloud
- Spring
- JPA
- go-core
- TEST
- toby
- java8
- frontend개발환경
- DesignPattern
- servlet
- Design Pattern
- javascript
- JavaScript Core
- code
- frontcode
- Kotlin
- Jackson
- MySQL
- OOP
- backend개발환경
- http
- db
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
