SSLHandshakeException이 발생한 HTTP 네트워크 상황
SSLHandshakeException은 HTTPS 기반 HTTP 통신에서 TLS 핸드셰이크가 정상적으로 완료되지 않았을 때 발생하는 예외입니다. 단순히 서버에 연결하지 못했다는 의미라기보다는, 클라이언트와 서버가 암호화 통신을 시작하기 전에 필요한 인증서 검증이나 TLS 협상이 실패했다는 의미에 가깝습니다.
실무에서는 외부 API를 호출하는 배치, 결제 승인 요청, 사내 인증 서버 연동, webhook 전송 같은 구간에서 자주 마주칩니다. 특히 Java 애플리케이션에서는 JDK가 사용하는 truststore와 서버 인증서 체인이 맞지 않을 때 아래와 같은 로그가 남는 경우가 많습니다.
javax.net.ssl.SSLHandshakeException: PKIX path building failed
sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target
이 메시지를 처음 보면 네트워크가 끊긴 것처럼 느껴질 수 있습니다. 하지만 위 로그의 핵심은 "요청 대상까지 유효한 인증 경로를 찾지 못했다"는 부분입니다. 즉, TCP 연결 자체보다 인증서 검증 단계에서 실패했다고 보는 편이 더 정확합니다.
HTTP 통신에서 SSLHandshakeException이 의미하는 것
HTTP 요청이 HTTPS로 전송될 때는 실제 요청 본문을 보내기 전에 TLS 핸드셰이크가 먼저 진행됩니다. 이 과정에서 서버는 자신의 인증서를 클라이언트에 전달하고, 클라이언트는 해당 인증서가 신뢰할 수 있는 인증기관에서 발급된 것인지 확인합니다.
Java 애플리케이션은 보통 JDK에 포함된 기본 truststore를 기준으로 인증서를 검증합니다. 여기서 서버 인증서의 루트 CA나 중간 CA가 truststore에 없거나, 인증서 체인이 올바르게 전달되지 않으면 SSLHandshakeException이 발생할 수 있습니다.
핸드셰이크 실패는 HTTP 응답 코드 문제가 아닙니다
이 부분은 자주 헷갈립니다. SSLHandshakeException은 일반적인 400, 401, 500 같은 HTTP 응답 코드와 성격이 다릅니다. HTTP 요청이 서버 애플리케이션까지 도달하기 전에 TLS 단계에서 막히기 때문입니다.
따라서 서버 로그에 요청 기록이 없을 수 있습니다. 클라이언트 입장에서는 외부 API 호출이 실패했는데, 상대 서버 담당자는 "요청이 들어온 기록이 없다"고 말할 수 있습니다. 이때는 애플리케이션 레벨의 API 오류가 아니라 네트워크 보안 계층의 문제로 분리해서 봐야 합니다.
SSLHandshakeException의 대표 원인
SSLHandshakeException은 하나의 원인으로만 발생하지 않습니다. HTTP 네트워크 통신 경로에서 인증서, TLS 버전, 프록시, JDK 버전이 함께 영향을 줄 수 있습니다. 그래서 로그 메시지를 먼저 보고 원인 후보를 나누는 방식이 좋습니다.
1. 서버 인증서를 Java truststore가 신뢰하지 못하는 경우
가장 흔한 유형은 PKIX path building failed입니다. 외부 서버의 인증서 체인을 Java가 신뢰하지 못할 때 발생합니다. 브라우저에서는 정상 접속되는데 Java 애플리케이션에서만 실패하는 경우도 있습니다.
브라우저와 Java 런타임은 인증서를 검증하는 기준 저장소가 다를 수 있습니다. 로컬 브라우저에서는 신뢰되는 인증서라도, 운영 서버의 JDK truststore에는 해당 루트 CA가 없을 수 있습니다. 특히 오래된 JDK를 쓰거나 사설 인증서를 사용하는 내부 시스템에서는 이 차이가 더 크게 드러납니다.
2. 서버가 중간 인증서를 제대로 전달하지 않는 경우
서버 인증서 자체는 유효하지만, 중간 인증서 체인이 누락되어도 문제가 생길 수 있습니다. 브라우저는 누락된 중간 인증서를 보완해서 처리하는 경우가 있지만, Java 클라이언트는 더 엄격하게 실패할 수 있습니다.
이 경우 클라이언트에서 무작정 인증서를 import하기보다, 먼저 서버가 전체 인증서 체인을 정상적으로 내려주고 있는지 확인하는 편이 좋습니다. 서버 설정 문제를 클라이언트 truststore 수정으로 덮어버리면 나중에 같은 문제가 다른 서비스에서 반복될 수 있습니다.
3. TLS 버전 또는 암호화 스위트가 맞지 않는 경우
클라이언트와 서버가 지원하는 TLS 버전이 맞지 않아도 핸드셰이크가 실패합니다. 예를 들어 서버는 TLS 1.2 이상만 허용하는데, 오래된 Java 런타임이나 설정이 낮은 TLS 버전으로만 통신하려고 하면 협상이 실패할 수 있습니다.
이때 로그에는 protocol_version, handshake_failure, no cipher suites in common 같은 메시지가 함께 나타날 수 있습니다. 인증서 문제와 다르게 이 경우에는 truststore에 인증서를 추가해도 해결되지 않습니다.
4. 사내 프록시나 보안 장비가 HTTPS를 가로채는 경우
회사 내부망이나 특정 서버 구간에서는 보안 장비가 HTTPS 트래픽을 검사하기 위해 자체 인증서를 끼워 넣는 경우가 있습니다. 개발자 PC에서는 정상인데 특정 서버에서만 실패한다면 이 가능성도 확인해야 합니다.
이 경우 Java 애플리케이션은 실제 외부 서버 인증서가 아니라 프록시 장비가 발급한 인증서를 보게 됩니다. 해당 인증서의 루트 CA가 Java truststore에 없으면 SSLHandshakeException이 발생합니다.
SSLHandshakeException 원인 추적 순서
SSLHandshakeException을 해결할 때는 애플리케이션 코드부터 수정하기보다, 먼저 통신 경로와 인증서 상태를 확인하는 편이 안전합니다. 저는 보통 아래 순서로 확인합니다.
1. 예외 로그의 마지막 원인을 확인합니다
Java 예외는 중첩되어 출력되는 경우가 많습니다. 겉으로는 SSLHandshakeException만 보이지만, 실제 원인은 아래쪽 caused by에 있는 경우가 많습니다.
Caused by: sun.security.validator.ValidatorException:
PKIX path building failed
Caused by: sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target
위 메시지라면 인증서 신뢰 체인 문제로 접근하면 됩니다. 반대로 protocol_version이나 handshake_failure가 보이면 TLS 버전과 cipher suite 쪽을 먼저 봐야 합니다. 로그의 마지막 원인을 기준으로 방향을 잡아야 불필요한 설정 변경을 줄일 수 있습니다.
2. openssl로 서버 인증서 체인을 확인합니다
서버가 어떤 인증서를 내려주는지 확인할 때는 openssl 명령이 유용합니다. 애플리케이션 코드와 분리해서 TLS 연결 상태를 먼저 볼 수 있기 때문입니다.
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts
여기서 확인할 부분은 인증서의 subject, issuer, 만료일, 중간 인증서 포함 여부입니다. 특히 SNI가 필요한 서버라면 -servername 옵션을 빼면 다른 인증서가 내려올 수 있습니다. 이 옵션을 누락한 채로 확인하면 실제 애플리케이션 통신과 다른 결과를 볼 수 있습니다.
3. curl로 HTTP 요청이 가능한지 분리해서 확인합니다
curl은 운영 서버에서 네트워크 경로를 확인할 때 자주 사용합니다. Java 애플리케이션 문제인지, 서버 환경의 네트워크 문제인지 분리하는 데 도움이 됩니다.
curl -v https://api.example.com/health
curl에서는 성공하지만 Java에서만 실패한다면 JDK truststore나 Java TLS 설정을 의심할 수 있습니다. 반대로 curl에서도 실패한다면 서버 인증서 체인, 방화벽, 프록시, DNS, 네트워크 경로 문제까지 함께 봐야 합니다.
4. Java SSL 디버그 로그를 활성화합니다
원인이 잘 보이지 않을 때는 Java SSL 디버그 로그를 켜고 핸드셰이크 과정을 확인합니다. 로그가 길게 나오기 때문에 운영 환경에서 장시간 켜두는 방식보다는 재현 가능한 환경에서 짧게 확인하는 편이 좋습니다.
-Djavax.net.debug=ssl,handshake
이 로그에서는 어떤 TLS 버전으로 협상하는지, 어떤 인증서가 전달되는지, 어느 단계에서 검증이 실패하는지 확인할 수 있습니다. 단순히 예외 메시지만 보는 것보다 훨씬 구체적인 판단이 가능합니다.
Java truststore에 인증서를 추가하는 방법
HTTP 네트워크 호출에서 인증서 신뢰 문제가 명확하다면, Java truststore에 필요한 인증서를 추가할 수 있습니다. 다만 이 방법은 원인을 정확히 확인한 뒤에 적용해야 합니다. 서버가 인증서 체인을 잘못 내려주는 상황이라면 서버 설정을 고치는 것이 먼저입니다.
인증서 파일 추출
openssl로 서버 인증서를 확인한 뒤 필요한 인증서를 파일로 저장합니다. 보통은 서버 인증서 자체보다 루트 CA 또는 중간 CA 인증서를 truststore에 넣어야 하는 경우가 많습니다.
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts < /dev/null
출력된 인증서 블록 중 필요한 부분을 별도 파일로 저장합니다. 파일에는 아래와 같은 BEGIN CERTIFICATE와 END CERTIFICATE 구간이 포함되어야 합니다.
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
keytool로 truststore에 추가
Java에서는 keytool 명령으로 인증서를 truststore에 추가할 수 있습니다. 운영 서버의 JDK 경로가 여러 개라면 실제 애플리케이션이 사용하는 JDK의 cacerts를 확인해야 합니다.
keytool -importcert \
-alias example-api-ca \
-file example-api-ca.crt \
-keystore $JAVA_HOME/lib/security/cacerts
기본 cacerts 비밀번호는 환경에 따라 다를 수 있지만, 일반적으로 변경하지 않았다면 changeit을 사용하는 경우가 많습니다. 다만 운영 환경에서는 기본 비밀번호 사용 여부보다 변경 이력과 배포 방식이 더 중요합니다. 서버마다 수동으로 넣는 방식은 누락되기 쉬우므로 이미지 빌드나 배포 스크립트에 포함하는 것이 관리하기 좋습니다.
별도 truststore를 사용하는 방식
JDK 기본 cacerts를 직접 수정하는 방식은 간단하지만, 운영 표준으로는 별도 truststore를 두는 편이 더 관리하기 좋습니다. 어떤 인증서를 왜 추가했는지 추적하기 쉽고, JDK 업그레이드 시 설정이 사라지는 문제도 줄일 수 있습니다.
keytool -importcert \
-alias example-api-ca \
-file example-api-ca.crt \
-keystore app-truststore.jks
애플리케이션 실행 시에는 아래처럼 truststore 경로를 명시합니다.
-Djavax.net.ssl.trustStore=/app/config/app-truststore.jks
-Djavax.net.ssl.trustStorePassword=changeit
이 방식은 Docker나 Kubernetes 환경에서도 적용하기 좋습니다. truststore 파일을 이미지에 포함하거나 Secret, ConfigMap, 볼륨 마운트 방식으로 관리할 수 있습니다. 팀 단위로 운영한다면 누가 봐도 인증서 의존성을 확인할 수 있는 구조가 유지보수에 좋습니다.
Spring Boot에서 확인할 부분
Spring Boot 애플리케이션에서 SSLHandshakeException이 발생했다면 어떤 HTTP 클라이언트를 쓰는지도 확인해야 합니다. RestTemplate, WebClient, Feign Client, Apache HttpClient, OkHttp 등에 따라 TLS 설정 위치가 달라질 수 있습니다.
RestTemplate 사용 시
기본 RestTemplate은 JVM의 기본 SSL 설정을 따르는 경우가 많습니다. 따라서 별도 SSLContext를 구성하지 않았다면 truststore 설정은 보통 JVM 옵션으로 적용합니다.
java \
-Djavax.net.ssl.trustStore=/app/config/app-truststore.jks \
-Djavax.net.ssl.trustStorePassword=changeit \
-jar app.jar
이 설정은 애플리케이션 전체 JVM에 영향을 줍니다. 여러 외부 API를 호출하는 서비스라면 특정 클라이언트에만 별도 SSLContext를 적용할지, 전체 JVM truststore로 통일할지 기준을 정하는 것이 좋습니다.
WebClient 사용 시
WebClient는 Reactor Netty 기반으로 사용하는 경우가 많습니다. 커스텀 SSL 설정이 들어가 있다면 JVM 옵션만으로 해결되지 않을 수 있습니다. 코드에서 SslContext를 별도로 구성하고 있는지 확인해야 합니다.
WebClient webClient = WebClient.builder()
.baseUrl("https://api.example.com")
.build();
기본 설정만 사용한다면 JVM truststore 영향권에 있는 경우가 많습니다. 하지만 Netty SslContext를 직접 주입했다면 해당 설정이 우선할 수 있으므로, HTTP 클라이언트 설정 코드를 함께 확인해야 합니다.
절대 먼저 하면 안 되는 해결 방식
SSLHandshakeException을 급하게 해결하다 보면 인증서 검증을 끄는 코드를 검색해서 붙이고 싶은 유혹이 있습니다. 개발 환경에서 원인 확인을 위해 일시적으로 사용할 수는 있지만, 운영 코드에 넣는 것은 피해야 합니다.
// 운영 코드에 이런 방식으로 인증서 검증을 우회하면 안 됩니다.
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}
};
이 코드는 서버 인증서를 검증하지 않겠다는 의미입니다. HTTPS를 쓰는 이유 중 하나가 서버 신뢰 검증인데, 이 부분을 제거하면 중간자 공격이나 잘못된 서버 연결을 애플리케이션이 구분하지 못합니다. 문법은 동작해도 보안 의도가 사라지는 코드입니다.
또 하나 조심할 부분은 curl의 -k 옵션입니다.
curl -k https://api.example.com/health
-k 옵션은 인증서 검증을 건너뛰고 연결을 시도합니다. 원인 분리용으로는 쓸 수 있지만, "curl -k로 되니까 네트워크는 정상"이라고만 판단하면 부족합니다. 인증서 검증을 포함한 정상 연결이 가능한지 별도로 확인해야 합니다.
적용 후 검증 방법
SSLHandshakeException을 해결했다면 단순히 애플리케이션이 한 번 성공하는지만 볼 것이 아니라, 어떤 방식으로 해결되었는지 남겨두는 것이 좋습니다. 인증서는 만료와 교체가 반복되는 자원이기 때문입니다.
1. Java 애플리케이션에서 실제 호출을 재시도합니다
먼저 동일한 서버 환경에서 애플리케이션을 재시작하고 외부 API 호출을 다시 확인합니다. 이때 기존 SSLHandshakeException이 사라졌는지, HTTP 응답 코드가 정상적으로 돌아오는지 확인합니다.
HTTP 200 OK
또는
HTTP 401 Unauthorized
여기서 401 같은 응답이 돌아온다면 적어도 TLS 핸드셰이크는 통과했다는 의미로 볼 수 있습니다. 인증이나 권한 문제는 그 다음 단계의 애플리케이션 이슈로 분리하면 됩니다.
2. truststore에 인증서가 정상 등록되었는지 확인합니다
추가한 인증서가 실제 truststore에 들어갔는지도 확인해야 합니다. alias를 기준으로 조회하면 됩니다.
keytool -list \
-keystore app-truststore.jks \
-alias example-api-ca
운영 환경에서는 이 명령 결과와 인증서 만료일을 함께 기록해두면 좋습니다. 나중에 인증서가 교체되었을 때 같은 문제를 다시 추적하는 시간을 줄일 수 있습니다.
3. 배포 환경별 차이를 확인합니다
로컬, 개발, 스테이징, 운영 서버가 서로 다른 JDK 이미지를 사용할 수 있습니다. 로컬에서는 해결됐는데 운영에서만 실패한다면 JDK 버전, 컨테이너 이미지, truststore 파일 경로, 실행 옵션을 다시 비교해야 합니다.
Docker 환경이라면 이미지 빌드 단계에서 truststore가 포함되었는지 확인합니다. Kubernetes에서는 Secret이나 ConfigMap으로 마운트한 파일 경로가 실제 JVM 옵션과 일치하는지도 봐야 합니다. 파일은 존재하지만 애플리케이션이 다른 경로를 보고 있는 경우도 적지 않습니다.
SSLHandshakeException 해결 시 정리해둘 체크리스트
HTTP 네트워크 연동에서 SSLHandshakeException이 발생하면 아래 항목을 순서대로 확인하면 원인을 좁히기 좋습니다.
- 예외 로그의 마지막 caused by 메시지를 확인했는가?
- PKIX 문제인지, TLS 버전 문제인지 구분했는가?
- openssl s_client로 서버 인증서 체인을 확인했는가?
- SNI가 필요한 서버에 -servername 옵션을 사용했는가?
- curl과 Java 애플리케이션 결과를 분리해서 비교했는가?
- 애플리케이션이 실제 사용하는 JDK와 truststore 경로를 확인했는가?
- 인증서 검증을 우회하는 코드가 운영 코드에 들어가지 않았는가?
- 인증서 추가 방식이 수동 작업에만 의존하지 않도록 정리했는가?
이 체크리스트는 특별한 도구보다 확인 순서를 정리하는 데 의미가 있습니다. SSLHandshakeException은 원인 후보가 넓기 때문에, 처음부터 코드를 고치기보다 네트워크와 인증서 검증 흐름을 나눠서 보는 것이 더 빠릅니다.
마무리: SSLHandshakeException은 네트워크 오류처럼 보이는 인증서 검증 문제입니다
SSLHandshakeException은 HTTP 호출 실패처럼 보이지만, 실제로는 HTTPS 통신을 시작하기 위한 TLS 핸드셰이크 단계에서 실패한 것입니다. 그래서 일반적인 API 오류와 같은 방식으로 접근하면 원인 추적이 길어질 수 있습니다.
가장 먼저 해야 할 일은 예외 로그를 보고 인증서 신뢰 문제인지, TLS 협상 문제인지 구분하는 것입니다. 그 다음 openssl, curl, Java SSL 디버그 로그를 활용해서 애플리케이션 코드 바깥의 문제를 분리해야 합니다.
해결 방법으로는 서버 인증서 체인 수정, JDK 업데이트, 별도 truststore 구성, 프록시 인증서 등록 등이 있습니다. 반대로 인증서 검증을 끄는 방식은 원인을 숨기는 임시 우회에 가깝기 때문에 운영 코드에서는 피하는 것이 좋습니다.
정리하면 SSLHandshakeException은 "HTTP 요청이 실패했다"가 아니라 "안전한 HTTP 통신을 시작하기 위한 신뢰 검증에 실패했다"라고 이해하면 됩니다. 이 관점으로 보면 로그를 읽는 방향도 달라지고, 해결 과정도 훨씬 명확해집니다.
'개발 > JAVA' 카테고리의 다른 글
| [JAVA] Java Connection reset 에러 발생 원인과 해결 과정 (0) | 2026.05.30 |
|---|---|
| [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 |
