TCP와 UDP 내부 동작의 핵심
TCP와 UDP의 연결 수립, 흐름 제어, 혼잡 제어, 종료 상태를 중심으로 네트워크 장애를 해석하는 기준을 정리한다. QUIC, ALB와 NLB, Keep-Alive, Nagle 알고리즘 같은 실무 연결 지점도 함께 짚는다.
Script Companion
오디오와 함께 스크립트 보기
- 01
TCP와 UDP는 HTTP, gRPC, WebSocket, DNS, 스트리밍 같은 상위 프로토콜 아래에 있는 전송 계층의 핵심이다. TCP/IP 4계층으로 보면 응용 계층은 데이터를 만들고, 전송 계층은 TCP나 UDP로 전달 방식을 정하며, 인터넷 계층은 IP로 경로를 잡고, 네트워크 접근 계층은 Ethernet이나 Wi-Fi로 실제 전송을 맡는다. 송신 측에서는 각 계층이 헤더를 붙이는 캡슐화가 일어나고, 수신 측에서는 헤더를 벗겨 위 계층으로 넘기는 디캡슐화가 일어난다. 프론트엔드에서 fetch API 한 줄만 보이더라도, 브라우저 엔진 안에서는 이런 계층과 TCP 연결 과정이 함께 움직인다.
- 02
TCP 연결은 3-Way Handshake로 시작한다. 클라이언트가 SYN으로 연결을 요청하고, 서버가 SYN-ACK로 수락과 역방향 확인을 보내며, 클라이언트가 ACK로 응답하면 연결이 열린다. 연결 뒤에는 흐름 제어가 중요해진다. Sliding Window는 수신자 버퍼 여유 공간을 수신 윈도우 rwnd 값으로 알리고, 송신자는 그 값을 넘어서 보낼 수 없다. 버퍼가 꽉 차면 rwnd는 0이 되고, 송신자는 전송을 멈춘 뒤 Zero Window Probe로 여유가 생겼는지 확인한다. Ethernet 기준 MTU는 1500 bytes이고, TCP 헤더 20B와 IP 헤더 20B를 빼면 데이터 크기 단위 MSS는 1460 bytes가 된다.
- 03
흐름 제어가 수신자 버퍼를 보호한다면, 혼잡 제어는 네트워크 전체가 막히지 않게 속도를 조절한다. 핵심 변수는 혼잡 윈도우 cwnd와 임계값 ssthresh이며, 실제 전송 윈도우는 cwnd와 rwnd 중 작은 값이다. Slow Start에서는 초기 cwnd를 1 MSS, 또는 최근 Linux 기준 10 MSS에서 시작해 RTT마다 2배로 늘린다. 임계값 ssthresh에 닿으면 Congestion Avoidance로 넘어가 RTT당 약 1 MSS씩 선형 증가한다. 손실이 감지되면 Fast Retransmit은 3번의 중복 ACK를 보고 타임아웃 전에 재전송하고, Fast Recovery는 cwnd를 줄이되 바로 Slow Start로 돌아가지는 않는다.
- 04
TCP 종료 상태에서는 TIME_WAIT과 CLOSE_WAIT을 구분해야 한다. TIME_WAIT은 연결 종료 뒤에도 2MSL 동안 기다리는 정상적인 마무리 상태다. 마지막 ACK가 유실됐을 때를 대비하고, 같은 포트로 새 연결이 생겼을 때 예전 세그먼트가 뒤늦게 섞이는 것을 막기 위해 필요하다. MSL은 세그먼트가 네트워크에 살아 있을 수 있는 최대 시간 60초이고, 2MSL은 120초다. 반면 CLOSE_WAIT은 상대가 먼저 연결을 닫았는데 애플리케이션이 소켓을 닫지 않은 상태다. 문서는 이를 거의 100% 애플리케이션 코드 버그로 본다. TIME_WAIT은 자동으로 줄지만, CLOSE_WAIT은 코드를 고쳐야 줄어든다.
- 05
UDP는 TCP와 반대로 연결 수립과 재전송, ACK, 순서 보장을 덜어내서 빠르다. TCP는 데이터 전송 전에 3-Way Handshake로 1.5 RTT가 필요하지만, UDP는 첫 패킷부터 0 RTT로 보낼 수 있다. 그래서 DNS 조회, 실시간 영상통화, 온라인 게임, DHCP처럼 지연이 더 중요한 상황에서 쓰인다. QUIC은 UDP 위에서 직접 패킷 재전송과 순서 보장을 구현하고, TLS 1.3을 기본 내장해 1-RTT로 암호화와 연결 수립을 함께 처리한다. HTTP/3의 기반이지만, 기업 네트워크나 방화벽이 UDP를 차단하면 성능 이점을 얻지 못하고 폴백될 수 있다.
- 06
TCP 보안과 성능 판단에서도 내부 동작은 그대로 드러난다. SYN Flood는 SYN을 대량으로 보내 half-open 연결 테이블을 채워 정상 연결을 못 받게 만드는 공격이다. AWS Shield Standard는 SYN Cookie로 대응하며, half-open 연결을 서버 메모리에 저장하지 않고 암호화된 시퀀스 번호를 SYN-ACK에 담는다. 혼잡 제어 쪽에서는 Linux 기본 알고리즘 CUBIC과 Google이 개발한 BBR이 대비된다. CUBIC은 패킷 손실을 보고 혼잡을 판단하지만, BBR은 RTT 변화와 대역폭을 기준으로 판단한다. 고지연 환경이나 CDN Origin 트래픽이 많은 경우 BBR 전환으로 처리량이 개선될 수 있다고 문서는 설명한다.
- 07
작은 지연을 만드는 대표 조합은 Nagle 알고리즘과 Delayed ACK다. Nagle 알고리즘은 작은 패킷이 이미 나간 뒤 ACK를 받지 못했으면 추가 데이터를 모아 큰 패킷으로 보내려 한다. Delayed ACK는 ACK만 담긴 빈 패킷을 줄이려고 최대 40~500ms까지 응답을 늦춘다. 둘이 만나면 송신자는 확인을 기다리고, 수신자는 확인을 미루는 교착이 생겨 매 요청마다 불필요한 지연이 붙을 수 있다. 해결 실마리는 TCP_NODELAY이며, 문서는 HTTP 서버, 특히 WebSocket이나 실시간 API에서는 setNoDelay(true)를 권장한다고 정리한다. TCP Keepalive와 HTTP Keep-Alive도 이름은 비슷하지만, 하나는 L4 생존 확인이고 다른 하나는 L7 연결 재사용이다.
- 08
AWS 인프라에서는 ALB와 NLB 선택이 TCP와 UDP 이해로 이어진다. ALB는 Layer 7에서 HTTP, HTTPS, gRPC를 처리하고 URL, 헤더, 쿠키 기준 라우팅을 한다. NLB는 Layer 4에서 TCP, UDP, TLS를 처리하고 IP와 Port 기준으로 라우팅하며, 정적 IP와 UDP, 극도로 낮은 지연시간이 필요할 때 선택한다. BackOps와 Nest.js 환경에서는 keepAliveTimeout과 ALB idle timeout이 특히 중요하다. ALB 기본 idle timeout은 60초인데 서버가 먼저 연결을 닫으면 ALB가 닫힌 소켓에 요청을 보내 ECONNRESET과 502 Bad Gateway가 발생할 수 있다. 문서는 headersTimeout, keepAliveTimeout, ALB idle timeout의 순서를 맞추는 것을 핵심으로 둔다.
- 09
트러블슈팅은 상태와 계층을 먼저 나누면 좁혀진다. Connection reset by peer는 상대가 RST 패킷으로 강제로 연결을 끊은 상태이며, 서버 재시작, 프로세스 종료, 방화벽이나 로드밸런서의 idle timeout, 서버 keepAliveTimeout 설정 불일치가 원인이 될 수 있다. TIME_WAIT 폭증은 HTTP keep-alive 없이 매 요청마다 새 TCP 연결을 만들고 닫을 때 흔하며, 연결 재사용과 Connection Pool이 근본 해결책이다. AWS VPC에서는 Security Group은 Stateful이지만 NACL은 Stateless라서, Inbound만 열고 Outbound Ephemeral Port 1024-65535를 막으면 응답이 돌아오지 못한다. 정리하면 TCP는 신뢰성과 순서를 위해 상태를 관리하고, UDP는 빠른 전송을 위해 신뢰성을 상위 계층이나 애플리케이션에 넘긴다.
같은 레이어