Java Connection reset 에러는 어떤 상황에서 발생할까
Java Connection reset 에러는 이미 연결된 TCP 커넥션이 상대방에 의해 강제로 종료되었을 때 주로 발생합니다. Java 코드 입장에서는 데이터를 읽거나 쓰는 도중 연결이 갑자기 끊긴 상태로 보이기 때문에 java.net.SocketException: Connection reset 형태의 예외가 남습니다.
이 에러가 까다로운 이유는 애플리케이션 코드 한 줄만 보고 원인을 단정하기 어렵다는 점입니다. 서버가 끊었을 수도 있고, 클라이언트가 먼저 연결을 닫았을 수도 있으며, 중간에 있는 로드밸런서나 프록시가 연결을 정리했을 수도 있습니다. 그래서 이 문제는 로그 한 줄보다 연결 흐름 전체를 보는 방식으로 접근해야 합니다.
java.net.SocketException: Connection reset
at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:401)
at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:434)
at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:137)
위 로그는 Java 애플리케이션이 소켓에서 데이터를 읽는 중 커넥션이 리셋되었다는 의미입니다. 여기서 중요한 점은 “Java가 연결을 끊었다”가 아니라 “Java가 사용하던 연결이 이미 끊긴 것을 감지했다”에 좀 더 가깝다는 점입니다.
Java Connection reset 에러의 대표 원인
Java Connection reset 에러의 원인은 보통 네트워크 계층, HTTP 클라이언트 설정, 서버 타임아웃, 중간 프록시 설정 중 하나에서 시작됩니다. 실무에서는 이 중 하나만 문제라기보다 여러 설정의 시간이 서로 맞지 않아 발생하는 경우가 많습니다.
클라이언트가 응답을 기다리다 연결을 닫은 경우
서버가 응답을 준비하는 시간이 길어졌는데 클라이언트의 read timeout이 더 짧게 설정되어 있으면 클라이언트가 먼저 연결을 닫을 수 있습니다. 이후 서버가 늦게 응답을 쓰려고 하면 이미 닫힌 커넥션에 데이터를 쓰게 되고, 이때 Connection reset 또는 Broken pipe 계열의 에러가 발생할 수 있습니다.
예를 들어 외부 API를 호출하는 Java 클라이언트에서 timeout을 너무 짧게 잡아두면 정상적으로 처리 가능한 요청도 중간에 끊길 수 있습니다.
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(3000)
.setConnectionRequestTimeout(3000)
.setSocketTimeout(3000)
.build();
위 설정에서 setSocketTimeout은 응답 데이터를 기다리는 시간과 관련이 있습니다. 외부 API가 가끔 3초 이상 걸리는 구조라면 이 값은 너무 공격적일 수 있습니다. timeout은 짧을수록 좋은 설정이 아니라, 호출 대상의 응답 특성과 실패 허용 기준에 맞춰 정해야 합니다.
Keep-Alive 커넥션이 재사용되는 과정에서 끊긴 경우
HTTP 클라이언트는 성능을 위해 커넥션 풀을 사용합니다. 한 번 연결한 TCP 커넥션을 재사용하면 매번 새 연결을 만드는 비용을 줄일 수 있습니다. 하지만 서버나 로드밸런서가 이미 닫아버린 커넥션을 클라이언트가 살아 있다고 판단하고 다시 사용하면 Connection reset이 발생할 수 있습니다.
이 경우에는 요청이 매번 실패하지 않고 간헐적으로 실패하는 특징이 있습니다. 같은 API를 호출해도 어떤 요청은 성공하고, 어떤 요청은 커넥션 리셋으로 실패합니다. 그래서 처음 보면 애플리케이션 로직 문제처럼 보이지만 실제로는 커넥션 풀의 idle timeout 설정이 원인일 수 있습니다.
PoolingHttpClientConnectionManager manager =
new PoolingHttpClientConnectionManager();
manager.setMaxTotal(200);
manager.setDefaultMaxPerRoute(50);
CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(manager)
.evictExpiredConnections()
.evictIdleConnections(30, TimeUnit.SECONDS)
.build();
idle connection을 적절히 정리하지 않으면 닫힌 연결을 재사용하는 문제가 생길 수 있습니다. 반대로 너무 짧게 정리하면 커넥션 재사용의 이점이 줄어듭니다. 이 값은 서버, 로드밸런서, 프록시의 keep-alive timeout보다 길게 두지 않는 편이 안전합니다.
로드밸런서나 프록시의 idle timeout과 맞지 않는 경우
Java 애플리케이션 앞단에 Nginx, ALB, API Gateway, 사내 프록시가 있는 구조에서는 중간 장비의 timeout 설정도 함께 봐야 합니다. 애플리케이션 서버는 커넥션이 유지된다고 생각하지만, 중간 프록시가 먼저 idle connection을 정리할 수 있습니다.
예를 들어 클라이언트 커넥션 풀의 keep-alive 시간이 120초인데 로드밸런서 idle timeout이 60초라면, 60초 이후 해당 연결은 중간에서 닫혔을 수 있습니다. 그런데 클라이언트가 그 연결을 90초 시점에 다시 사용하면 커넥션 리셋이 발생할 가능성이 있습니다.
클라이언트 keep-alive: 120초
로드밸런서 idle timeout: 60초
서버 keep-alive timeout: 75초
문제 가능성:
클라이언트가 60초 이후에도 기존 커넥션을 재사용하려고 시도
이런 구조에서는 클라이언트의 idle connection 제거 시간을 중간 장비 timeout보다 짧게 두는 것이 좋습니다. 연결을 오래 잡고 있는 것보다, 죽은 연결을 재사용하지 않도록 관리하는 쪽이 더 중요합니다.
Connection reset 에러를 로그에서 확인하는 방법
Connection reset 에러를 볼 때는 예외 메시지 하나만 보지 말고, 요청 방향과 발생 위치를 먼저 나눠야 합니다. 우리 서비스가 외부 API를 호출하다가 발생한 것인지, 외부 클라이언트가 우리 API를 호출하다가 발생한 것인지에 따라 확인해야 할 지점이 달라집니다.
외부 API 호출 중 발생한 경우
우리 Java 서버가 외부 API를 호출하는 클라이언트 역할이라면, 우선 HTTP client timeout과 connection pool 설정을 확인합니다. 이때 connect timeout, read timeout, connection request timeout을 구분해서 봐야 합니다.
connect timeout:
상대 서버와 TCP 연결을 맺는 데 허용하는 시간
read timeout:
연결 후 응답 데이터를 기다리는 시간
connection request timeout:
커넥션 풀에서 사용 가능한 커넥션을 가져오기까지 기다리는 시간
실무에서는 이 세 가지를 모두 “timeout”으로만 부르면서 설정을 뭉뚱그리는 경우가 있습니다. 하지만 Connection reset은 read 과정이나 재사용된 커넥션에서 자주 드러나기 때문에, 어떤 timeout이 실제 흐름에 영향을 주는지 구분해야 합니다.
우리 API 서버에서 발생한 경우
반대로 우리 서버 로그에 Connection reset이 찍힌다면 클라이언트가 연결을 먼저 끊었을 가능성도 봐야 합니다. 사용자가 브라우저를 닫았거나, 모바일 앱 네트워크가 전환되었거나, 앞단 프록시가 요청을 중단했을 수 있습니다.
org.apache.catalina.connector.ClientAbortException: java.io.IOException: Connection reset by peer
Tomcat 기반 Spring Boot 서버에서 위와 같은 로그가 보인다면, 서버가 응답을 쓰는 도중 클라이언트 연결이 끊긴 상황일 수 있습니다. 모든 ClientAbortException을 서버 장애로 볼 필요는 없습니다. 다만 특정 API에서 반복적으로 발생한다면 응답 시간이 길거나, 클라이언트 timeout과 서버 처리 시간이 맞지 않는지 확인해야 합니다.
Connection reset 원인 추적 과정
Connection reset 에러를 해결할 때는 무작정 timeout을 늘리는 방식으로 접근하지 않는 것이 좋습니다. timeout을 늘리면 당장 에러가 줄어든 것처럼 보일 수 있지만, 실제로는 느린 요청을 더 오래 붙잡게 되어 다른 문제가 가려질 수 있습니다.
발생 위치를 먼저 분리한다
가장 먼저 해야 할 일은 에러가 inbound 요청에서 발생했는지 outbound 요청에서 발생했는지 나누는 것입니다. inbound는 외부에서 우리 서버로 들어오는 요청이고, outbound는 우리 서버가 다른 시스템으로 보내는 요청입니다.
확인 순서
1. 에러가 발생한 서버가 클라이언트 역할인지 서버 역할인지 확인
2. 같은 시간대의 access log 확인
3. 외부 API 호출 로그의 응답 시간 확인
4. 로드밸런서 또는 프록시 로그 확인
5. 커넥션 풀과 timeout 설정 비교
이 과정을 거치면 원인 후보가 줄어듭니다. Java 코드에서 예외가 발생했다고 해서 항상 Java 코드가 원인은 아닙니다. 네트워크 연결의 주도권은 양쪽 모두에게 있고, 중간 장비도 연결을 닫을 수 있습니다.
에러 발생 패턴을 확인한다
Connection reset이 항상 발생하는지, 특정 시간 이후 첫 요청에서만 발생하는지, 특정 외부 API에서만 발생하는지 확인해야 합니다. 항상 발생한다면 인증, TLS, 네트워크 차단, 잘못된 endpoint 설정 가능성이 큽니다. 반대로 간헐적으로 발생한다면 idle connection 재사용이나 timeout 불일치를 의심하는 것이 좋습니다.
항상 실패:
- endpoint 오류
- 방화벽 또는 보안 그룹 차단
- TLS 설정 불일치
- 상대 서버가 연결을 즉시 종료
간헐적 실패:
- idle connection 재사용
- keep-alive timeout 불일치
- read timeout 부족
- 중간 프록시의 연결 정리
간헐적인 에러는 재현이 어렵기 때문에 로그에 요청 ID, 호출 대상, 소요 시간, 재시도 여부를 함께 남겨두는 것이 좋습니다. 나중에 같은 문제가 반복될 때 단순히 “네트워크 문제”로 넘기지 않고 패턴을 비교할 수 있습니다.
Java Connection reset 해결 방법
Java Connection reset 에러를 해결할 때는 원인별로 접근해야 합니다. timeout만 늘리거나 재시도만 추가하면 문제의 형태가 바뀔 뿐, 근본 원인이 남아 있을 수 있습니다.
HTTP 클라이언트 timeout을 명확하게 설정한다
외부 API 호출에는 connect timeout과 read timeout을 반드시 명시하는 편이 좋습니다. 기본값에 의존하면 라이브러리 변경이나 설정 누락에 따라 예상하지 못한 대기 시간이 생길 수 있습니다.
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000)
.setConnectionRequestTimeout(3000)
.setSocketTimeout(10000)
.build();
CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.evictExpiredConnections()
.evictIdleConnections(30, TimeUnit.SECONDS)
.build();
위 예시는 Apache HttpClient 기준입니다. 중요한 부분은 숫자 자체가 아니라 각 timeout의 역할을 나눠 설정했다는 점입니다. 외부 API의 평균 응답 시간, 최대 허용 시간, 실패 시 대체 흐름을 고려해서 값을 정해야 합니다.
커넥션 풀의 idle connection을 정리한다
커넥션 풀을 사용하는 경우 idle connection 정리는 꼭 확인해야 합니다. 오래 쉬고 있던 커넥션은 서버나 중간 장비에서 이미 닫혔을 수 있기 때문입니다.
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.evictExpiredConnections()
.evictIdleConnections(20, TimeUnit.SECONDS)
.build();
로드밸런서 idle timeout이 60초라면 클라이언트의 idle connection 제거는 그보다 짧게 가져가는 식으로 맞추는 편이 안전합니다. 이 설정은 Connection reset이 특정 시간 간격 뒤 첫 요청에서 주로 발생할 때 특히 효과가 있습니다.
재시도는 조건을 제한해서 적용한다
Connection reset은 일시적인 네트워크 문제일 수 있기 때문에 재시도가 도움이 될 때가 있습니다. 하지만 모든 요청에 무조건 재시도를 붙이면 중복 처리 문제가 생길 수 있습니다. 조회성 요청은 비교적 안전하지만, 결제, 주문, 포인트 적립 같은 변경 요청은 멱등성 보장이 먼저입니다.
재시도 적용 전 확인할 것
- 같은 요청을 두 번 보내도 안전한가
- 멱등키를 사용할 수 있는가
- 상대 서버가 중복 요청을 어떻게 처리하는가
- 재시도 간격과 최대 횟수는 제한되어 있는가
실무에서는 재시도를 넣는 것보다 “어떤 요청에 재시도를 넣지 않을지”를 정하는 일이 더 중요할 때가 많습니다. 특히 상태를 변경하는 API는 요청 ID나 idempotency key 없이 재시도하면 Connection reset보다 더 복잡한 정합성 문제가 생길 수 있습니다.
Spring Boot에서 확인할 설정 포인트
Spring Boot 환경에서 Java Connection reset 에러가 보인다면 내장 Tomcat 설정과 HTTP client 설정을 분리해서 봐야 합니다. 서버로 들어오는 요청의 timeout과 외부로 나가는 요청의 timeout은 서로 다른 영역입니다.
Tomcat keep-alive 설정 확인
Spring Boot가 Tomcat을 사용한다면 서버 커넥터의 keep-alive 관련 설정을 확인할 수 있습니다. 이 값은 클라이언트와 서버 사이의 연결 유지 방식에 영향을 줍니다.
server:
tomcat:
keep-alive-timeout: 20s
max-keep-alive-requests: 100
connection-timeout: 5s
이 설정은 사용하는 Spring Boot와 Tomcat 버전에 따라 지원 방식이 다를 수 있습니다. 적용 전에는 현재 프로젝트 버전에서 해당 프로퍼티가 실제로 반영되는지 확인해야 합니다. 설정 파일에 적혀 있다고 해서 항상 런타임에 적용되는 것은 아닙니다.
RestTemplate 또는 WebClient 설정 확인
RestTemplate을 사용한다면 내부에 어떤 ClientHttpRequestFactory를 쓰는지 봐야 합니다. 기본 설정 그대로 사용하면 timeout이 의도대로 들어가지 않는 경우가 있습니다.
@Bean
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(5000);
factory.setReadTimeout(10000);
return new RestTemplate(factory);
}
WebClient를 사용하는 경우에는 Reactor Netty의 connection timeout, response timeout, connection pool 설정을 별도로 확인해야 합니다. 같은 Spring 애플리케이션 안에서도 RestTemplate과 WebClient의 설정 방식은 다르므로 팀 내에서 공통 HTTP client 설정을 관리하는 편이 유지보수에 좋습니다.
Connection reset 해결 후 확인해야 할 것
Java Connection reset 에러를 수정한 뒤에는 단순히 로그가 줄었는지만 보면 부족합니다. 어떤 원인에 대해 어떤 설정을 바꿨고, 그 결과 요청 성공률과 재시도 횟수, 외부 API 응답 시간이 어떻게 바뀌었는지 함께 확인해야 합니다.
적용 후 확인 항목
- Connection reset 발생 건수
- 특정 API 또는 특정 외부 도메인에 집중되는지
- timeout 발생 건수
- 재시도 성공률
- 평균 응답 시간과 긴 응답의 비율
- 커넥션 풀 고갈 여부
특히 재시도를 추가했다면 최종 성공률만 보지 말고 재시도 자체가 얼마나 발생하는지도 봐야 합니다. 재시도로 가려진 실패가 늘어나고 있다면 연결 문제는 여전히 남아 있는 것입니다.
Connection reset 에러를 줄이기 위한 실무 기준
Java Connection reset 에러는 완전히 0으로 만들기 어려운 유형의 문제입니다. 네트워크는 언제든 끊길 수 있고, 클라이언트는 요청 도중 이탈할 수 있으며, 중간 장비는 idle connection을 정리할 수 있습니다. 중요한 것은 이 에러를 모두 장애로 확대하지 않으면서도 반복 패턴은 놓치지 않는 것입니다.
실무에서는 다음 기준으로 정리하는 편이 좋습니다.
1. 외부 API 호출에는 timeout을 명시한다.
2. 커넥션 풀을 사용한다면 idle connection 정리 정책을 둔다.
3. 로드밸런서와 클라이언트 keep-alive 시간을 비교한다.
4. 재시도는 멱등성이 보장되는 요청에 제한한다.
5. Connection reset 로그를 요청 방향별로 분리해서 본다.
6. 같은 API에서 반복되면 응답 시간과 timeout 설정을 함께 확인한다.
이 기준을 잡아두면 Connection reset을 볼 때마다 매번 처음부터 추적하지 않아도 됩니다. 팀에서 공통 HTTP client 설정을 만들고, timeout과 retry 정책을 문서화해두면 비슷한 문제가 다른 서비스로 번지는 것도 줄일 수 있습니다.
마무리: Java Connection reset 에러는 연결 흐름으로 봐야 한다
Java Connection reset 에러는 단순한 예외 메시지처럼 보이지만, 실제로는 TCP 연결이 어느 지점에서 닫혔는지를 추적해야 하는 문제입니다. 서버 코드만 봐서는 원인을 찾기 어렵고, 클라이언트 timeout, 커넥션 풀, keep-alive, 로드밸런서 설정까지 함께 봐야 합니다.
해결 과정의 핵심은 에러 발생 위치를 분리하고, 간헐적 패턴인지 지속적 실패인지 확인한 뒤, timeout과 idle connection 정책을 맞추는 것입니다. 재시도는 보조 수단으로 두되, 멱등성 없는 요청에 무리하게 적용하지 않는 편이 안전합니다.
Connection reset을 제대로 다루려면 “왜 끊겼는지”보다 먼저 “누가, 언제, 어떤 연결을 끊었는지”를 확인해야 합니다. 이 관점으로 접근하면 로그 한 줄에 끌려다니지 않고 원인 후보를 차분하게 줄여갈 수 있습니다.
'개발 > JAVA' 카테고리의 다른 글
| [JAVA] 트랜잭션 분리 안 해서 장애 난 사례: 외부 API 호출과 DB 저장을 같은 트랜잭션에 묶으면 생기는 일 (0) | 2026.05.24 |
|---|---|
| [JAVA] Hibernate flush 타이밍 때문에 발생한 문제와 실무에서 확인하는 방법 (0) | 2026.05.23 |
| [JAVA] Lock wait timeout exceeded 에러가 발생하는 원인과 해결 방법 (0) | 2026.05.22 |
| [JAVA] Java DB Connection Pool 부족으로 장애가 났을 때 원인 분석과 해결 방법 (0) | 2026.05.21 |
| [JAVA] JPA N+1 문제 발견하고 해결한 과정: 원인 분석부터 Fetch Join 적용까지 (0) | 2026.05.20 |
