이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- IP 도달성, TCP port listener, firewall 허용을 어떻게 분리해서 확인하는가?
connection refused,timeout,no route to host,connection reset은 각각 어떤 계층의 단서인가?- LB listener port, target port, container port, application bind address가 어긋나면 어떤 장애가 나는가?
개요
IP는 목적 host나 인터페이스를 찾는 주소이고, port는 그 host 안에서 어떤 프로세스나 서비스 endpoint로 보낼지를 결정한다. 백엔드에서 “서버가 떠 있다”는 말은 실제로는 특정 network namespace 안에서 특정 IP와 port에 listener가 있다는 뜻이다.
- IP는 host를 찾고 port는 host 안의 프로세스나 서비스 endpoint를 찾는다.
- LB, container, Kubernetes Service를 거치면 client port, listener port, target port, container port가 다를 수 있다.
- 보안 그룹과 firewall은 IP와 port 단위로 traffic을 허용하거나 차단한다.
왜 백엔드 개발자가 알아야 하는가
앱이 정상 시작했는데도 사용자가 접속하지 못하는 경우가 많다. 이때 코드를 보기 전에 IP와 port 계약을 먼저 확인해야 한다.
- 앱이
127.0.0.1:8080에만 bind하면 같은 host 내부에서는 되지만 container 밖이나 LB에서는 안 될 수 있다. - LB는 443으로 받고 target은 8080으로 보내는데 health check port가 80이면 target이 unhealthy가 된다.
- Docker
EXPOSE 8080은 문서에 가깝고, 실제 외부 접근은 port publish가 필요하다. - Kubernetes Service
port,targetPort, PodcontainerPort가 어긋나면 503/connection refused가 난다. - outbound API 장애가 외부 API 문제가 아니라 NAT/firewall/ephemeral port 문제일 수 있다.
요청 흐름에서의 위치
DNS가 IP 후보를 반환한 뒤, 실제 연결을 시도하는 지점이다.
DNS answer
→ client route table
→ firewall/security group/NACL
→ LB listener port
→ target group port
→ host network namespace
→ process listen socket이 단계에서 실패하면 HTTP status code가 아예 없을 수 있다. 브라우저는 ERR_CONNECTION_REFUSED, ERR_CONNECTION_TIMED_OUT처럼 보여 주고, 서버 application log에는 아무것도 남지 않는다.
원리
서버 프로세스는 socket을 만들고 특정 주소에 bind한 뒤 listen한다.
| Bind address | 의미 | 실무 영향 |
|---|---|---|
127.0.0.1:8080 | loopback에서만 수신 | 같은 host 내부만 접근 가능 |
0.0.0.0:8080 | 모든 IPv4 interface 수신 | container/LB target 접근 가능 |
[::]:8080 | IPv6 all interface | dual-stack 동작 방식 확인 필요 |
10.0.1.23:8080 | 특정 private IP 수신 | interface 변경 시 실패 가능 |
연결 실패 메시지는 중요한 단서다.
| 증상 | 흔한 의미 | 다음 확인 |
|---|---|---|
| connection refused | 대상이 RST로 거절, listener 없음 또는 port 닫힘 | ss -lntp, target port, app bind |
| timeout | SYN 응답 없음, 방화벽/drop/routing 문제 가능 | security group, firewall, route, NACL |
| no route to host | 로컬 route나 gateway 문제 가능 | ip route get, subnet, VPN |
| connection reset | 연결 중 RST, proxy/app/idle timeout/정책 종료 가능 | RST 보낸 주체, proxy log |
핵심 판단 기준
- IP reachability와 port reachability를 분리한다. ping이 된다고 HTTP가 되는 것은 아니다.
- bind address, listener port, container publish, LB target port, health check port를 한 줄로 그린다.
- inbound와 outbound를 구분한다. 들어오는 요청과 외부 API로 나가는 요청은 firewall, NAT, route가 다르다.
- IPv4와 IPv6를 둘 다 확인한다. AAAA record가 있으면 IPv6 listener와 firewall도 있어야 한다.
- 운영 문서에는 “앱은 8080”만 쓰지 말고 “LB 443 → target 8080 → container 8080 → app 0.0.0.0:8080”처럼 적는다.
실무에서 자주 만나는 문제
- container는 8080에서 듣는데 LB target port가 80으로 되어 있다.
- localhost 기준으로만 테스트해서 외부 network namespace를 놓친다.
- IPv6 AAAA record가 열려 있는데 IPv6 listener가 없다.
- Spring Boot가
server.address=127.0.0.1로 떠서 container 밖에서 접근할 수 없다. - cloud security group은 열렸지만 OS firewall이나 Kubernetes NetworkPolicy가 막는다.
- health check path는 맞는데 health check port가 앱 port와 다르다.
- NAT 뒤 outbound 호출이 많아 특정 destination으로만 연결이 실패한다.
- private subnet의 서버가 인터넷 API를 호출하는데 NAT gateway route가 없다.
이 계층의 문제는 application log가 비어 있는 경우가 많다. 그래서 “로그가 없다”는 사실 자체가 요청이 앱까지 오지 않았다는 중요한 증거가 된다.
디버깅 방법
ss -lntp로 listen port를 본다.nc -vz host port로 원격 port를 확인한다.- cloud security group과 host firewall을 모두 확인한다.
ip route get <ip>로 어느 interface와 gateway로 나가는지 본다.curl --connect-timeout으로 TCP 연결 단계의 지연을 분리한다.- container 안과 host에서 같은 명령을 실행해 namespace 차이를 본다.
- LB target health reason과 앱 process listener를 동시에 확인한다.
ip route get 203.0.113.10
ss -lntp | grep ':8080'
nc -vz api.example.com 443
curl -v --connect-timeout 2 http://10.0.1.23:8080/healthremote에서 refused가 나고 target host의 ss -lntp에도 listener가 없으면 앱 bind/port 문제에 가깝다. remote에서 timeout이고 target host에는 listener가 있으면 firewall, security group, route, network policy를 우선 본다.
코드로 확인하기
Spring Boot와 container를 같이 쓸 때는 bind address와 port mapping을 명시적으로 확인한다.
# application.yml
server:
address: 0.0.0.0
port: 8080docker run --rm -p 8080:8080 my-api:latest
curl -fsS http://127.0.0.1:8080/actuator/healthKubernetes에서는 이름이 비슷해도 의미가 다르다.
apiVersion: v1
kind: Service
metadata:
name: order-api
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: order-api위 예시는 client가 Service의 80 port로 접근하면 Pod의 8080 port로 전달한다. Pod가 실제로 8080에 listen하지 않으면 Service 설정이 맞아도 실패한다.
주니어가 자주 하는 오해
- 서버 프로세스가 떠 있으면 외부에서도 접속된다고 생각한다.
- 어떤 IP에 bind했고 어떤 port가 공개되었는지가 별도 문제다.
- ping이 되면 서비스도 된다고 생각한다.
- ICMP 도달성과 TCP port listener는 다르다.
EXPOSE 8080이면 Docker가 자동으로 port를 열어 준다고 생각한다.EXPOSE는 metadata이고 외부 publish는-p나 orchestration 설정이 필요하다.
- timeout과 refused를 같은 접속 실패로 본다.
- refused는 거절 응답이 온 것이고 timeout은 응답이 오지 않은 것이다.
시니어의 설계 판단 기준
- 서비스마다 inbound port map과 outbound dependency map을 문서화한다.
- health check port/path/protocol은 배포 pipeline에서 검증한다.
- private/public subnet, NAT, firewall, NetworkPolicy를 코드 배포와 같은 변경 관리 대상으로 본다.
- IPv6를 켜면 monitoring, firewall, rate limit, access log의 client IP 처리까지 준비한다.
- port 변경은 app config, container, service, LB, firewall, runbook을 함께 바꾸는 작업으로 취급한다.
인프라 협업 포인트
- 인프라 팀에 요청할 때 source IP/CIDR, destination IP/host, port, protocol, 방향(inbound/outbound)을 명확히 준다.
- “API가 안 됩니다”보다 “10.0.2.15에서 10.0.4.20:8080 TCP connect timeout”이 훨씬 빠르다.
- LB 502/503이면 target port, health check port, target security group, app listener를 함께 본다.
- 방화벽 변경에는 검증 명령과 rollback rule을 같이 둔다.
실전 팁
- 개인 프로젝트에서도 앱 시작 로그에 bind address와 port를 남긴다.
- outbound dependency는 DNS 이름만이 아니라 실제 destination port와 보안 그룹 의존성을 문서화한다.
localhost테스트와 container 내부 테스트와 LB 경유 테스트를 구분한다.- connection refused가 나면 app부터 재시작하기보다 listener와 target port를 먼저 본다.
위험 신호!
- 앱 port, container port, Service targetPort, LB target port가 문서에 없다.
- production에서
127.0.0.1bind 설정을 쓴다. - IPv6 DNS record가 있는데 IPv6 listener와 firewall을 아무도 확인하지 않았다.
- 인프라 요청에 source/destination/port/protocol이 없다.
- application log만 보고 port/firewall 문제를 배제한다.
확인 질문
확인 질문
connection refused와timeout은 어떻게 다른가?
- refused는 대상이 연결을 거절했다는 응답에 가깝고, timeout은 응답이 돌아오지 않아 방화벽, route, drop 가능성을 열어 두어야 한다.
127.0.0.1:8080에만 bind한 앱이 LB 뒤에서 실패하는 이유는 무엇인가?
- loopback interface에서만 듣기 때문에 외부 interface나 container network에서 접근할 수 없기 때문이다.
- 인프라 팀에 port 허용을 요청할 때 최소한 무엇을 줘야 하는가?
- source, destination, port, protocol, 방향, 목적, 검증 명령, rollback 조건을 줘야 한다.