TCP와 UDP
IP는 상자를 목적지 건물까지 “최선을 다해” 보낼 뿐이다. 도중에 상자가 사라지거나, 순서가 뒤바뀌거나, 두 번 도착해도 IP는 책임지지 않는다. 그런데도 우리가 내려받은 파일은 한 바이트도 틀리지 않고, 수십억 명이 같은 길을 나눠 써도 인터넷은 멈추지 않는다. 이 장은 그 마법을 부리는 전송 계층, 특히 인터넷에서 가장 많이 쓰이는 두 배송 규칙인 TCP와 UDP 이야기다.
- 포트 번호와 소켓으로 한 컴퓨터 안의 여러 앱을 구분하는 법을 안다.
- UDP가 8바이트 헤더만 붙이고 그냥 보내는 이유와 쓰임새를 설명한다.
- 3방향 핸드셰이크와 4방향 종료를 순서 번호·ACK 번호와 상태 이름으로 따라간다.
- 누적 ACK, 타임아웃, 중복 ACK 3개에 의한 빠른 재전송이 손실을 복구하는 방식을 체험한다.
- 슬라이딩 윈도우와 “처리율 ≤ 윈도우/RTT” 관계를 시뮬레이터로 확인한다.
- 흐름 제어와 혼잡 제어를 구분하고, 슬로 스타트·AIMD 톱니·CUBIC·BBR의 차이를 안다.
- 헤드 오브 라인 블로킹이 무엇이고 QUIC이 연결 준비 시간과 함께 이를 어떻게 줄이는지 이해한다.
전송 계층의 일: 포트와 소켓
IP 주소(7장)는 패킷을 컴퓨터까지 데려다준다. 하지만 컴퓨터 안에는 브라우저 탭 여러 개, 메신저, 음악 앱, 백그라운드 업데이트가 동시에 통신하고 있다. 도착한 패킷을 이 중 누구에게 줄지 정하는 것이 전송 계층(3장의 4계층)의 첫 번째 일이다. 그 표시가 포트 번호(port number), 0부터 65535까지의 16비트 숫자다.
IP 주소가 아파트 건물 주소라면 포트 번호는 호수다. 택배 기사는 건물까지만 책임지고, 관리실(운영체제)이 호수를 보고 각 집(앱)에 나눠 준다. 웹 서버는 “443호”, 메일 서버는 “25호”처럼 모두가 아는 호수에 산다. 반대로 손님(클라이언트)은 그때그때 비어 있는 임시 호수를 받아 답장 받을 곳으로 쓴다.
“IP 주소 + 포트 번호” 한 쌍을 소켓(socket) 주소라 하고, 프로그램이 네트워크와 데이터를 주고받는 창구를 그냥 소켓이라고도 부른다. TCP 연결 하나는 (프로토콜, 보내는 IP, 보내는 포트, 받는 IP, 받는 포트) 다섯 값, 즉 5-튜플로 구분된다. 그래서 서버는 443번 포트 하나로 수만 명의 손님과 동시에 따로따로 대화할 수 있다.
국제 인터넷 번호 관리 기관(IANA)은 포트를 세 구역으로 나눈다. 0~1023은 잘 알려진 포트(well-known ports)로 대표 서비스가 쓰고, 1024~49151은 등록 포트, 49152~65535는 클라이언트가 잠깐 빌려 쓰는 임시 포트다(리눅스는 기본으로 32768~60999를 쓴다).
| 포트 | 프로토콜 | 서비스 | 이 책에서 |
|---|---|---|---|
| 22 | TCP | SSH (원격 접속) | 22장 |
| 25 / 587 | TCP | SMTP (메일 전달 / 메일 제출) | 10장 |
| 53 | UDP·TCP | DNS (이름 → 주소) | 10장 |
| 67 / 68 | UDP | DHCP (주소 자동 배정) | 7장 |
| 80 | TCP | HTTP (암호화 없는 웹) | 10장 |
| 123 | UDP | NTP (시간 맞추기) | 23장 |
| 443 | TCP · UDP | HTTPS (TCP), HTTP/3 (UDP 위 QUIC) | 이 장, 10장 |
| 993 | TCP | IMAPS (메일함 읽기) | 10장 |
| 179 | TCP | BGP (라우터끼리 경로 교환) | 8장 |
전송 계층의 두 대표 선수는 성격이 정반대다. UDP(User Datagram Protocol)는 “상자에 호수만 적어서 그냥 보낸다.” TCP(Transmission Control Protocol)는 “연결을 맺고, 번호를 매기고, 받았다는 확인을 받고, 잃어버리면 다시 보내고, 길이 막히면 속도를 줄인다.” 일반 우편과 등기 우편의 차이다.
UDP: 일단 보낸다
UDP 헤더는 겨우 8바이트, 필드는 네 개뿐이다. 보내는 포트, 받는 포트, 길이, 체크섬. 연결을 맺지도, 순서를 챙기지도, 재전송하지도 않는다. 받는 쪽이 데이터를 받았는지조차 모른다. 대신 군더더기가 없다. 첫 패킷부터 바로 데이터를 실을 수 있고, 손실 하나 때문에 뒤따르는 데이터가 멈추지도 않는다.
그렇다면 UDP는 어디에 쓸까? “조금 잃어도 괜찮지만 늦으면 안 되는” 일, 또는 “신뢰성은 내가 알아서 챙기겠다”는 일이다.
- DNS(10장): 질문 하나, 답 하나로 끝나는 짧은 대화. 연결을 맺는 데 드는 왕복 시간이 아깝다. 답이 안 오면 앱이 다시 물어본다.
- 온라인 게임·음성·영상 통화: 0.3초 전 캐릭터 위치를 다시 받아 봐야 쓸모없다. 새 데이터가 곧 오래된 데이터를 대신한다(24장).
- 실시간 방송·일부 스트리밍: 지연이 짧아야 하는 라이브 방송, IPTV 멀티캐스트.
- QUIC과 HTTP/3: UDP를 “빈 상자”로 쓰고, 그 위에 TCP보다 똑똑한 신뢰성 기능을 직접 얹었다. 이 장 끝에서 다룬다.
UDP에는 혼잡 제어가 없어서, 잘못 만든 앱은 망이 막혀도 계속 퍼붓는다. 또 보내는 주소를 속이기 쉬워, 작은 질문으로 큰 답을 남에게 보내게 만드는 증폭 공격(DNS·NTP 반사 공격, 22장)에 악용되곤 한다. 그래서 UDP 위의 진지한 프로토콜(QUIC, WebRTC)은 모두 스스로 혼잡 제어와 주소 확인을 넣는다.
TCP 연결: 세 번 악수하고 네 번 인사한다
TCP는 데이터를 보내기 전에 먼저 연결을 맺는다. 전선이 생기는 것은 아니고, 양쪽이 “지금부터 이 번호로 시작하는 바이트 흐름을 주고받자”고 약속하고 상태를 기억해 두는 것이다. 이 약속을 3방향 핸드셰이크(three-way handshake)라 한다.
- SYN: 클라이언트가 “연결하자. 내 바이트 번호는 1000부터 시작한다.”(seq=1000)
- SYN-ACK: 서버가 “좋다. 네 1000번은 받았으니 다음엔 1001번을 기다린다(ack=1001). 내 번호는 5000부터다.”(seq=5000)
- ACK: 클라이언트가 “네 5000번 받았다, 다음은 5001번.”(ack=5001) 이제 양쪽 모두 상대의 시작 번호를 안다.
여기서 순서 번호(sequence number)는 패킷 번호가 아니라 바이트 번호다. 100바이트를 seq=1001로 보내면 그 데이터는 1001~1100번 바이트이고, 받는 쪽은 ack=1101(“1100번까지 다 받았다, 다음은 1101”)로 답한다. SYN과 FIN은 데이터가 없어도 번호를 하나 차지한다. 시작 번호를 0이 아니라 무작위로 고르는 이유는, 예전 연결의 늦게 도착한 패킷과 헷갈리지 않고, 공격자가 번호를 추측해 가짜 패킷을 끼워 넣지 못하게 하기 위해서다.
ss -tan 명령으로 실제 상태를 볼 수 있다). 끊을 때는 양쪽이 각자 “나는 더 보낼 게 없다(FIN)”를 말하고 확인받아야 하므로 네 번이 된다. 서버의 ACK와 FIN은 보낼 데이터가 없으면 하나로 합쳐지기도 한다.먼저 끊자고 한 쪽은 마지막 ACK를 보낸 뒤에도 바로 사라지지 않고 TIME_WAIT 상태로 잠시(최대 세그먼트 수명의 두 배, 2×MSL. 리눅스는 60초) 기다린다. 마지막 ACK가 사라져 상대가 FIN을 다시 보내면 답해 줘야 하고, 같은 5-튜플로 새 연결을 바로 열었다가 옛 연결의 늦은 패킷이 섞여 들어오는 사고도 막아야 하기 때문이다.
TCP 연결의 일생: 한 단계씩 진행하기
서버는 SYN을 받으면 SYN_RCVD 상태의 “반쯤 열린 연결”을 기억해 둔다. 공격자가 가짜 주소로 SYN만 수백만 개 보내면 이 기억 공간이 꽉 차 진짜 손님을 못 받는다. 이 공격을 SYN 플러드라 하고, 상태를 저장하는 대신 시작 번호 속에 정보를 암호처럼 숨겨 두는 SYN 쿠키로 막는다(22장).
신뢰성: 확인하고, 기다리고, 다시 보낸다
TCP 신뢰성의 뼈대는 단순하다. 받는 쪽은 누적 ACK(cumulative ACK)로 “여기까지는 빈틈없이 다 받았다”를 알린다. 1~2번, 4~5번을 받고 3번이 빠졌다면 ACK는 계속 “3번을 기다린다”다. 보내는 쪽은 두 가지 신호로 손실을 알아챈다.
- 타임아웃: 보낸 뒤 일정 시간(재전송 타임아웃(RTO, retransmission timeout)) 안에 ACK가 없으면 사라졌다고 본다. RTO는 측정한 왕복 시간(RTT)의 평균과 흔들림으로 계산하는데(RFC 6298), 넉넉하게 잡아야 하므로 보통 RTT보다 한참 길다. 표준 최솟값은 1초, 리눅스는 200 ms다.
- 중복 ACK 3개: 3번이 빠진 채 4, 5, 6번이 도착하면 받는 쪽은 “3번 기다림”을 매번 반복한다. 같은 ACK를 세 번 더(중복 ACK 3개) 받으면, 보내는 쪽은 타이머를 기다리지 않고 곧바로 3번을 다시 보낸다. 이것이 빠른 재전송(fast retransmit)이다.
왜 하필 세 개일까? 패킷은 가끔 순서만 바뀌어 도착하기도 한다. 중복 ACK 한두 개는 단순한 순서 뒤바뀜일 수 있지만, 세 개가 쌓이면 진짜 손실일 가능성이 높다는 경험칙이다. 요즘 TCP는 여기에 SACK(선택적 ACK, “4~6번은 받았다”를 따로 알림)와 시간 기반 손실 감지(RACK)를 더해 여러 개가 한꺼번에 빠져도 빠르게 복구한다.
손실 복구: 빠른 재전송 vs 타임아웃
슬라이딩 윈도우: 확인을 기다리며 쉬지 않기
가장 단순한 신뢰성 규칙은 “하나 보내고, ACK가 올 때까지 기다리고, 다음 것을 보낸다”다. 이를 정지-대기(stop-and-wait)라 한다. 문제는 속도다. 서울–미국 서부처럼 RTT가 150 ms인 길에서는 1,500바이트 하나 보내고 150 ms를 논다. 회선이 1 Gbps여도 처리율은 1,500×8 / 0.15 ≈ 80 kbps, 1990년대 전화 모뎀 수준이다.
해법은 ACK를 기다리는 동안에도 여러 개를 연달아 보내는 것이다. “확인받지 않은 채 보낼 수 있는 양”을 슬라이딩 윈도우(sliding window)라 한다. 맨 앞 조각의 ACK가 오면 창이 오른쪽으로 한 칸 미끄러지고, 새로 창 안에 들어온 조각을 보낸다. 한 RTT 동안 창 하나만큼 보낼 수 있으므로 다음 관계가 성립한다.
대역폭-지연 곱(BDP)은 “길 위에 떠 있을 수 있는 데이터의 양”이다. 1 Gbps × 100 ms = 12.5 MB가 늘 길 위에 떠 있어야 회선이 쉬지 않는다. 원래 TCP 헤더의 윈도우 칸은 16비트라 최대 64 KB였기 때문에, 1992년에 윈도우 스케일 옵션(최대 1 GB)이 추가되었다.
슬라이딩 윈도우: 창 크기 · 손실 · RTT
윈도우가 64 KB로 고정된 오래된 TCP로 RTT 200 ms인 해외 서버에서 파일을 받는다. 집 회선을 100 Mbps에서 1 Gbps로 바꾸면 다운로드 속도는?
위 시뮬레이터에서 RTT를 200 ms로 두고 윈도우를 바꿔 보자. 처리율 ≤ 윈도우/RTT.
흐름 제어와 혼잡 제어: 누가 브레이크를 밟나
윈도우를 무작정 키우면 두 곳이 넘친다. 하나는 받는 쪽의 버퍼다. 앱이 데이터를 천천히 읽으면 수신 버퍼가 찬다. 받는 쪽은 매 ACK마다 “지금 내 버퍼에 이만큼 여유가 있다”는 수신 윈도우(rwnd)를 적어 보내고, 보내는 쪽은 그 이상 보내지 않는다. 이것이 흐름 제어(flow control)다.
다른 하나는 중간의 네트워크다. 가장 좁은 구간(병목)의 라우터 큐가 넘치면 패킷이 버려진다. 문제는 아무도 병목이 얼마나 넓은지 알려 주지 않는다는 점이다. 그래서 보내는 쪽이 스스로 추정한 값, 혼잡 윈도우(cwnd, congestion window)를 따로 관리한다. 이것이 혼잡 제어(congestion control)다. 실제로 보낼 수 있는 양은 둘 중 작은 쪽이다.
도로가 얼마나 비었는지 모르는 운전자 수천 명이 각자 속도를 정한다고 하자. 규칙은 이렇다. “처음엔 조심스럽게 들어가되 빠르게 늘린다. 막힘이 없으면 조금씩 더 낸다. 접촉 사고(손실)가 보이면 즉시 절반으로 줄인다.” 모두가 이 규칙을 지키면, 중앙 관제탑 없이도 도로가 꽉 차되 마비되지는 않는 상태에 저절로 모인다. 1986년 인터넷이 실제로 마비되는 “혼잡 붕괴”를 겪은 뒤, 밴 제이콥슨이 TCP에 넣은 규칙이 바로 이것이다.
TCP의 고전적인 혼잡 제어는 세 단계로 움직인다.
- 슬로 스타트(slow start): 처음엔 작은 cwnd(요즘은 10 MSS, 약 14 KB)로 시작해 ACK가 올 때마다 1씩, 즉 RTT마다 두 배로 늘린다. 이름과 달리 지수적으로 빠르게 늘어난다. “시작값이 작다”는 뜻이다.
- 혼잡 회피: cwnd가 문턱값 ssthresh에 닿으면 RTT마다 1 MSS씩 직선으로 늘린다.
- 손실 반응: 중복 ACK 3개면 cwnd를 절반으로(Reno), 타임아웃이면 1로 되돌리고 슬로 스타트부터 다시 한다.
“더할 때는 조금씩(더하기), 줄일 때는 크게(곱하기)” 규칙을 AIMD(additive increase, multiplicative decrease)라 한다. 이 규칙 덕분에 cwnd 그래프는 특유의 톱니 모양이 되고, 여러 흐름이 병목을 나눠 쓸 때 저절로 공평한 몫으로 수렴한다.
혼잡 윈도우의 톱니: Tahoe · Reno · CUBIC
Reno, 흐름 1개, 병목 용량 40일 때 라우터 버퍼를 0에서 100으로 늘리면 무슨 일이 생길까?
혼잡 제어 시뮬레이터에서 버퍼 슬라이더를 양 끝으로 옮겨 이용률과 큐 지연을 비교하자.
| 알고리즘 | 손실 신호 | 늘리는 방식 | 줄이는 방식 | 쓰이는 곳 |
|---|---|---|---|---|
| Tahoe (1988) | 손실 | 슬로 스타트 → +1/RTT | 모든 손실에 cwnd = 1 | 역사적 원형 |
| Reno / NewReno (1990~) | 손실 | 슬로 스타트 → +1/RTT | 중복 ACK 3개: ×½, 타임아웃: 1 | 교과서 표준, 오래된 시스템 |
| CUBIC (2006~) | 손실 | 마지막 손실 시점의 크기를 향해 3차 곡선으로 | ×0.7 | 리눅스·윈도우·macOS 기본 |
| BBR (2016~) | 대역폭·RTT 측정 | 측정한 병목 대역폭 × 최소 RTT에 맞춤 | 손실에 크게 반응하지 않음 | 구글·유튜브 등 서버, QUIC 구현 |
CUBIC은 RTT가 길고 넓은 망에서 Reno의 “RTT마다 +1”이 너무 느리다는 문제를 풀었다. 손실이 났던 크기(Wmax)를 기억해 두고, 그 근처까지는 빠르게 올라갔다가 근처에서는 조심스럽게, 넘어서면 다시 빠르게 탐색하는 3차 함수 모양으로 늘린다. 증가 속도가 RTT가 아니라 실제 시간에 묶여 있어 RTT가 다른 흐름끼리도 덜 불공평하다.
손실 기반 알고리즘은 큐를 꽉 채워 패킷이 버려져야 비로소 속도를 줄인다. 구글의 BBR(Bottleneck Bandwidth and RTT)은 발상을 바꿨다. 실제로 전달된 속도의 최댓값으로 병목 대역폭을, 관찰된 왕복 시간의 최솟값으로 순수 전파 지연을 측정하고, 둘을 곱한 BDP만큼만 길 위에 띄운다. 큐를 채우지 않으니 지연이 낮고, 무작위 손실이 있는 무선·장거리 망에서도 속도가 잘 유지된다. 대신 다른 알고리즘과 섞였을 때의 공평성 문제로 v2, v3로 개선이 이어지고 있다. 큐가 넘치도록 커진 버퍼가 지연을 망치는 버퍼블로트와 그 측정법은 23장에서 다룬다.
헤드 오브 라인 블로킹과 QUIC
TCP는 앱에 하나의 순서 있는 바이트 흐름을 준다. 이 약속이 때로는 족쇄가 된다. 웹 페이지 하나는 HTML, CSS, 자바스크립트, 이미지 수십 개를 동시에 받는데, HTTP/2(10장)는 이것들을 TCP 연결 하나에 섞어 보낸다. 그러다 패킷 하나가 사라지면, 그 뒤에 도착한 패킷이 전혀 다른 파일의 것이어도 TCP는 빈칸이 채워질 때까지 앱에 넘겨 주지 않는다. 계산대 맨 앞 손님 한 명 때문에 줄 전체가 멈추는 이 현상을 헤드 오브 라인 블로킹(head-of-line blocking, HOL)이라 한다.
패킷 하나를 잃으면: TCP 위 HTTP/2 vs QUIC 위 HTTP/3
QUIC은 이 문제와 연결 준비 시간을 함께 풀기 위해 구글이 만들고 IETF가 2021년 표준화한(RFC 9000) 전송 프로토콜이다. 특징은 다음과 같다.
- UDP 위에서 동작: 새 전송 프로토콜을 운영체제 커널과 전 세계 방화벽·NAT 장비가 받아들이게 하기는 사실상 불가능하다. 그래서 이미 어디서나 통과되는 UDP 상자에 담고, 신뢰성·혼잡 제어는 앱 쪽 라이브러리에서 구현한다. 덕분에 업데이트도 앱처럼 빠르다.
- TLS 1.3 내장: 전송 핸드셰이크와 암호화 핸드셰이크(22장)를 하나로 합쳐 1 RTT에 연결과 암호화가 끝난다. 전에 접속한 서버라면 첫 패킷에 요청을 실어 보내는 0-RTT도 된다. 헤더 대부분까지 암호화해 중간 장비가 엿보거나 건드리지 못한다.
- 독립 스트림: 연결 하나 안에 여러 스트림이 있고 순서는 스트림마다 따로 챙긴다. 패킷 하나가 사라져도 그 스트림만 기다린다.
- 연결 이전: TCP 연결은 IP 주소와 포트로 구분되므로 Wi-Fi에서 LTE로 바뀌면 끊긴다. QUIC은 연결 ID로 구분하므로 주소가 바뀌어도 대화가 이어진다.
첫 응답까지 걸리는 시간: RTT가 길수록 왕복 수가 중요하다
2025년 기준 주요 브라우저와 대형 서비스(구글, 유튜브, 메타, 클라우드플레어 등)는 HTTP/3를 기본으로 지원하고, 웹 트래픽의 상당 부분(대략 4분의 1~3분의 1)이 QUIC으로 오간다. 다만 일부 회사망은 UDP 443번을 막기 때문에, 브라우저는 QUIC이 안 되면 조용히 TCP로 되돌아간다. TCP는 사라지지 않는다. 데이터센터 내부, 파일 전송, 수많은 기존 프로토콜은 여전히 TCP 위에 있다.
핵심 정리
- 전송 계층은 포트 번호로 한 컴퓨터 안의 앱을 구분한다. 연결은 프로토콜·양쪽 IP·양쪽 포트의 5-튜플로 구분된다.
- UDP는 8바이트 헤더만 붙여 그냥 보낸다. DNS, 게임, 음성·영상 통화, 그리고 QUIC의 바탕이 된다.
- TCP는 SYN → SYN-ACK → ACK로 연결을 맺고, FIN/ACK를 양쪽이 주고받아 끊는다. 순서 번호와 ACK 번호는 바이트 단위다.
- 손실은 타임아웃 또는 중복 ACK 3개(빠른 재전송)로 감지해 다시 보낸다. 꼬리 손실은 중복 ACK가 생기지 않아 느리다.
- 슬라이딩 윈도우로 ACK를 기다리는 동안에도 보낸다. 처리율 ≤ 윈도우/RTT이므로 윈도우는 BDP만큼 커야 한다.
- 흐름 제어(rwnd)는 받는 쪽 버퍼를, 혼잡 제어(cwnd)는 네트워크를 지킨다. 슬로 스타트와 AIMD가 톱니를 그리고, 오늘날 기본값은 CUBIC, 측정 기반 BBR도 널리 쓰인다.
- QUIC은 UDP 위에서 TLS 1.3을 내장해 1-RTT/0-RTT로 연결하고, 독립 스트림으로 헤드 오브 라인 블로킹을 줄이며, 연결 ID로 네트워크가 바뀌어도 이어진다.
확인 퀴즈
클라이언트가 seq=2001로 500바이트를 보냈고 모두 잘 도착했다. 서버가 돌려보낼 ACK 번호는?
DNS 질의와 온라인 게임이 주로 UDP를 쓰는 가장 큰 이유는?
보내는 쪽이 같은 ACK 번호를 연달아 네 번(원래 1번 + 중복 3번) 받았다. TCP가 하는 일은?
RTT 50 ms, 윈도우 32 KB인 TCP 연결의 최대 처리율에 가장 가까운 것은?
흐름 제어와 혼잡 제어에 대한 설명으로 옳은 것은?
HTTP/2를 TCP 위에서 쓸 때 패킷 하나가 손실되면 생기는 일과, QUIC이 이를 줄이는 방법을 바르게 짝지은 것은?