mLog 명판이 있는 전화 교환대에서 연결선을 꽂고 전달할 편지를 따로 두는 흰색 3D 유령 교환원

Spring Boot 연결 오류 진단: TCP 포트 테스트와 HTTP 415를 구분하는 방법

네트워크 Spring Boot 2026년 9월 13일

외부 연동이 실패했을 때 먼저 구분할 것은 “포트에 연결하지 못했는가”와 “연결한 상대가 요청을 거절했는가”다. 둘은 고칠 설정도, 확인할 담당자도 다르다.

예를 들어 HTTP 415를 받았다면 요청을 해석한 HTTP 구성 요소가 응답한 것이다. 반대로 응답이 없다고 해서 방화벽 차단이 확정되지는 않는다. DNS·경로·TCP·TLS·프록시·원격 애플리케이션 중 어디서 멈췄는지 추가 증거가 필요하다.

이 글은 2026-09-13 공식 문서를 확인하고 Java 표준 API로 로컬 실험을 수행해 작성했다. 회사 서버에서 발생한 장애를 해결했다는 기록은 아니다. 외부 연동망·TLS·RestClient 통합 실행은 검증하지 않았으며, 실제 실행한 항목은 별도로 표시한다.

1. 먼저 요청의 경로를 적는다

브라우저에서 누른 테스트 버튼이 Spring Boot를 거쳐 외부로 나간다면 연결은 하나가 아니다. 브라우저→Spring Boot 구간과 Spring Boot→외부 시스템 구간이 있다. 중간에 프록시가 있으면 구간이 더 늘어난다.

따라서 브라우저가 받은 상태 코드만 보고 외부 시스템이 응답했다고 단정하면 안 된다. 진단 컨트롤러가 내부 예외를 잡아 항상 HTTP 200으로 감싸거나, 게이트웨이가 대신 오류를 만들 수도 있다.

먼저 다음 정보를 모은다.

  • 실제 외부 호출을 수행한 서버·컨테이너와 실행 시각
  • 승인된 목적지의 호스트명·포트·HTTP 또는 HTTPS 여부
  • HTTP 프록시 사용 여부와 연결하려는 상대가 프록시인지 원 서버인지
  • 외부 호출의 예외 원인 체인 또는 HTTP 응답 상태
  • 요청을 따라갈 수 있는 상관관계 ID

토큰·비밀번호·업무 전문은 이 단계의 진단 자료에 필요하지 않다. 원문 전체를 로그에 남기기보다 필요한 분류와 안전한 식별자만 보존한다.

2. RestClient와 TCP 테스트는 목적이 다르다

Spring의 RestClient는 동기식 HTTP 클라이언트다. 임의의 원시 TCP 전문을 송수신하는 도구가 아니다. 요청 팩토리와 실제 HTTP 클라이언트 구현에 따라 연결·타임아웃 설정도 달라질 수 있다. Spring REST Clients

HTTP 요청의 본문에 바이너리를 넣는 것과, TCP 소켓에 특정 전문을 직접 쓰는 것은 다르다. application/octet-stream을 지정해도 HTTP 메서드·헤더·상태 코드가 없는 원시 TCP 통신으로 바뀌지 않는다.

연동 명세에서 우선 확인할 것은 다음이다.

  • HTTP라면 URL, 메서드, Content-Type, 인코딩, 인증 방식, 성공 판정 기준
  • 원시 TCP라면 접속 주소, 포트, 메시지 경계, 길이 필드, 문자 인코딩, 응답 규칙
  • 어느 방식이든 TLS 사용 여부와 연결 종료·재시도 규칙

TCP 연결만 검사할 때는 업무 전문을 보내지 않는 작은 소켓 테스트로 범위를 제한할 수 있다. 다만 연결 자체도 상대 로그·보안 모니터링에 남을 수 있으므로 승인된 한 목적지에 필요한 횟수만 실행한다.

3. 결과가 무엇을 증명하는지 구분한다

관측 알 수 있는 것 아직 알 수 없는 것
DNS 실패 이름 해석이 완료되지 않음 목적지 포트의 실제 상태
TCP connect 성공 선택한 주소·포트로 그 시점에 연결 성립 TLS·HTTP·인증·업무 처리 성공
연결 거부 계열 연결 시도가 실패함 서비스 미기동인지 중간 정책 거부인지
연결 시간 초과 제한 시간 안에 connect가 끝나지 않음 어떤 장비가 원인인지
읽기 시간 초과 연결 뒤 지정한 읽기 대기에서 제한 초과 상대 업무 처리 성공·실패 여부
HTTP 415 HTTP 응답자가 요청 형식을 지원하지 않아 거절 최종 업무 서버까지 도달했는지

Java ConnectException은 원격 주소·포트에 연결하는 도중의 오류이며, 전형적인 예가 리스너가 없는 경우다. 예외 이름만으로 방화벽 상태를 확정하는 코드는 피한다. Java ConnectException

Google Cloud의 연결 점검 문서도 주소·포트·리스너·클라이언트와 서버의 방화벽·경로를 나누어 확인한다. 하나의 timeout 메시지로 네트워크 전체를 판정하지 않는 접근이다. Google Cloud 한국어 연결 점검

4. Java 표준 API로 한 번만 TCP 연결을 검사한다

다음은 HTTP 컨트롤러가 아니라 관리자용 작은 Java 프로그램이다. 승인받은 목적지만 직접 입력한다. 웹 요청의 hostport를 그대로 전달하는 방식으로 배포하지 않는다.

import java.io.IOException;
import java.net.*;

public class TcpProbe {
    public static String probe(String host, int port, int timeoutMs) {
        if (host == null || host.isBlank() || port < 1 || port > 65535
                || timeoutMs < 1 || timeoutMs > 10000) {
            throw new IllegalArgumentException("Invalid destination or timeout");
        }
        try {
            InetAddress address = InetAddress.getByName(host);
            try (Socket socket = new Socket(Proxy.NO_PROXY)) {
                socket.connect(new InetSocketAddress(address, port), timeoutMs);
                return "TCP_CONNECTED";
            }
        } catch (UnknownHostException e) {
            return "DNS_FAILED";
        } catch (SocketTimeoutException e) {
            return "CONNECT_TIMEOUT";
        } catch (ConnectException e) {
            return "CONNECT_FAILED";
        } catch (IOException e) {
            return "IO_FAILED";
        }
    }

    public static void main(String[] args) {
        if (args.length != 3) {
            throw new IllegalArgumentException("Expected: host port timeout-ms");
        }
        System.out.println(probe(args[0], Integer.parseInt(args[1]),
                Integer.parseInt(args[2])));
    }
}

JDK에서 위 코드를 TcpProbe.java로 저장하고 실행할 수 있다. 다음 숫자는 실행 형식의 예시이며 운영 권장값이 아니다. 127.0.0.1:18080에 자신의 테스트 리스너를 먼저 준비한 경우에만 이 주소가 의미가 있다.

java TcpProbe.java 127.0.0.1 18080 1000

코드는 애플리케이션 데이터를 쓰거나 읽지 않고 연결 뒤 소켓을 닫는다. Proxy.NO_PROXY는 Java 소켓에 직접 연결을 명시한 것이므로 실제 RestClient가 사용하는 HTTP 프록시 경로와 같다고 가정하면 안 된다. 프록시가 필수인 조직에서는 승인 없이 직접 연결을 시도하지 않는다.

TCP_CONNECTED는 서비스 건강 상태가 아니라 좁은 연결 결과다. 예를 들어 앞단 로드밸런서가 연결을 받았지만 뒤의 업무 서버는 실패할 수도 있다.

이 간소화 CLI는 네트워크 실패도 문자열로 출력한다. 프로세스 종료 코드 0만 보고 연결 성공으로 판단하지 말고 출력 분류를 확인한다. 운영 모니터링에 연결하려면 별도의 종료 코드·오류 처리 규약을 정해야 한다.

5. timeout 숫자 하나가 전체 제한 시간은 아니다

위 코드에는 두 가지 중요한 제한이 있다.

첫째, InetAddress.getByName()이 이름을 해석한 뒤에 connect()를 호출한다. 따라서 connect에 넣은 시간은 DNS를 포함한 프로그램 전체 실행 제한이 아니다. 또한 여기서는 선택된 주소 하나를 시험할 뿐, 호스트명의 모든 IPv4·IPv6 주소를 검증하지 않는다. Java InetAddress

둘째, 연결 시간 제한과 읽기 시간 제한은 다르다. Socket.connect(endpoint, timeout)의 제한은 연결 단계에 적용된다. setSoTimeout()은 소켓 입력 스트림의 읽기 대기에 적용되며, 이미 시작된 전체 업무를 취소하는 장치가 아니다. 이 예제는 읽지 않으므로 일반 점검 함수에는 setSoTimeout()을 넣지 않았다. 값 0은 무한 대기로 해석될 수 있어 예제는 양수만 받는다. Java Socket

실제 HTTP 클라이언트는 커넥션 풀에서 기다리는 시간까지 따로 있을 수 있다. 따라서 “timeout 1초를 넣었으니 모든 경우 1초에 종료한다”는 설명은 피한다. 구현체·DNS·풀 대기·TLS·응답 대기의 경계를 확인해야 한다.

6. 이번에 실제로 확인한 로컬 결과

2026-09-13 작업 환경의 OpenJDK 17.0.20에서 같은 probe() 함수와 별도 테스트 코드를 실행했다. 테스트는 127.0.0.1의 임시 포트만 사용했으며 회사망이나 외부 시스템을 조회하지 않았다.

실험 구성 실제 결과
로컬 ServerSocket이 듣고 있는 포트에 접속 TCP_CONNECTED
서버 측에서 점검 연결의 입력을 읽음 데이터 없이 EOF, EOF_NO_BYTES
로컬 리스너 종료 뒤 같은 포트에 접속 CONNECT_FAILED
연결은 받되 데이터를 보내지 않는 서버에서 읽기 대기 READ_TIMEOUT
connect 제한값 0 입력 REJECTED

이 실험은 “TCP 연결 성공”과 “읽기 성공”이 다르다는 것을 확인한다. 외부 방화벽이 패킷을 버리는 연결 시간 초과는 재현하지 않았다. TLS·HTTP 415·Spring 컨트롤러·RestClient 호출도 이 로컬 테스트에 포함되지 않았다.

리드 타임아웃 실험은 별도의 클라이언트에서 setSoTimeout(100) 후 읽기를 호출했다. 일반 probe()는 여전히 읽지 않는다. 100은 재현용 설정이지 서비스에 권장하는 제한 시간이 아니다. 닫힌 임시 포트는 다른 프로세스가 재사용할 가능성이 있어 모든 환경에서 동일한 결과를 보장하지 않는다.

위 결과는 본문과 동일한 probe() 함수에 별도의 로컬 테스트 코드를 붙여 얻었다. 본문 CLI에는 자체 테스트 옵션이 없다.

7. 반드시 실제 호출이 나가는 위치에서 확인한다

내 PC에서 성공한 결과는 서버에서 성공한 결과가 아니다. 서버 호스트에서 성공해도 컨테이너의 DNS·네트워크·출구 정책은 다를 수 있다. Spring Boot가 컨테이너 안에 있다면 그 실행 환경을 기준으로 승인된 점검 방법을 선택한다.

localhost는 지금 코드를 실행하는 환경 자신을 가리킨다. 원격 상대 주소를 넣어야 하는데 이를 사용하면 엉뚱한 대상을 점검하게 된다. 프록시를 이용한다면 직접 목적지 검사와 프록시를 통과하는 실제 요청을 나누어 기록한다.

DB 접속과 터널 경로의 차이는 기존 Windows·Neon 5432 연결과 SSH 터널에서 별도로 다뤘다. 이번 테스트는 SSH 터널을 만들거나 조직 정책을 우회하는 절차가 아니다.

8. TCP가 확인된 다음 HTTP 415를 본다

HTTP 415는 요청한 리소스와 메서드에서 지원하지 않는 콘텐츠 형식을 이유로 거절한 상태다. Content-Type이나 Content-Encoding 때문일 수도 있고, 실제 데이터를 검사한 결과일 수도 있다. RFC 9110 §15.5.16

이 단계에서는 명세와 실제 요청의 메서드·경로·헤더·바이트 인코딩을 대조한다. application/octet-stream을 쓰라는 명세라면 JSON 객체 직렬화가 아닌 요구된 바이트 형식인지 확인한다. Content-Type만 바꾸면 어떤 전문이든 맞게 된다고 생각하지 않는다.

반대로 415가 나오지 않았다는 사실은 방화벽 차단의 증거가 아니다. 다른 HTTP 오류, TLS 실패, 접속 실패, 응답 유실 또는 예외 포장이 먼저 일어날 수 있다. 원래 예외와 응답을 분리해 확인한다.

502가 프록시에서 발생했다면 NPM 502 진단으로 프록시와 업스트림 사이를 확인한다. 브라우저에서 CORS 메시지가 보인다면 Vue·IBSheet CORS와 프록시처럼 브라우저의 정책 오류와 서버 간 연결을 구분한다.

9. 진단 기능을 만들다가 새 위험을 만들지 않는다

서버 접속 권한이 없다고 누구나 임의의 호스트·포트를 입력하는 진단 API를 열면 안 된다. 서버가 대신 내부 목적지에 접근하는 기능이 될 수 있다. OWASP도 SSRF 방어에서 허용 대상과 네트워크 경계 통제를 강조한다. OWASP SSRF 예방

꼭 관리 기능이 필요하다면 대상은 서버 설정에서 승인된 고정 목록으로 관리하고, 사용자 입력은 그 목록의 식별자만 선택하게 한다. 관리자 인증·권한·호출 빈도 제한·감사 기록·폐기 시점도 정한다. HTTP 진단에 리디렉션을 추가하면 변경된 목적지도 통제해야 한다.

이번 예제에는 그런 웹 API를 제공하지 않았다. 실제 연동 테스트에서도 원인을 모르는 상태로 운영 전문을 반복 전송하거나 인증서 검증을 끄지 않는다. 응답이 없었다고 업무가 실행되지 않았다는 보장은 없으므로, 쓰기 요청의 재시도는 멱등성과 결과 조회 절차를 먼저 정해야 한다.

10. 담당자에게 전달할 최소 점검 기록

다음은 실측 결과가 아니라 기록 양식이다. 주소·네트워크 정보는 승인된 담당자에게만 전달하고 공개 글에는 비식별화한다.

확인 시각 및 시간대:
실행 위치(서버/컨테이너):
승인된 목적지 식별자:
선택된 대상 주소와 포트:
직접 연결/프록시 경유:
DNS 결과:
TCP 연결 결과:
TLS 또는 HTTP 확인 여부:
HTTP 상태 또는 예외 분류:
상관관계 ID:
미확인 범위:

“방화벽 열어 주세요”보다 “이 실행 위치에서 이 목적지로 TCP 연결 단계가 실패했고 HTTP 응답은 아직 확인하지 못했다”가 더 정확한 요청이다. 연결 성공·프로토콜 성공·업무 성공을 분리하면 불필요한 설정 변경과 반복 호출을 줄일 수 있다.

태그

mLog

8년 이상 풀스택 개발자로 일하고 있습니다. Spring Boot, PostgreSQL, Redis, Vue·TypeScript와 Docker·Linux 서버를 다루며, 직접 운영하고 해결한 내용을 공식 문서와 실행 결과를 바탕으로 정리합니다.