네트워크 학습 노트 목차

UDP — 가볍게, 빠르게, 책임은 앱이

TCP는 신뢰성을 위해 *핸드셰이크·순서번호·재전송·흐름/혼잡 제어*라는 많은 절차와 상태를 짊어졌다. 그런데 그 모든 게 항상 필요할까? 화상 통화를 떠올려 보자 — 0.2초 전의 음성 한 조각이 길에서 사라졌다면, 그걸 재전송받아 뒤늦게 트는 것보다 그냥 버리고 지금 음성을 트는 게 낫다. 여기서는 신뢰성보다 지연이 중요하다. 이런 경우를 위한 가벼운 전송 계층이 **UDP**다.

UDP가 하는 일 — 사실 거의 안 한다

UDP는 IP의 best-effort를 거의 그대로 위층에 노출한다. 헤더라곤 출발/목적 포트, 길이, 체크섬이 전부다.

다이어그램 로딩 중…

핸드셰이크가 없고(보내고 싶을 때 그냥 보낸다), 순서 보장이 없고, 재전송이 없고, 흐름·혼잡 제어없다. UDP가 더해 주는 건 사실상 포트(어느 프로그램에게 줄지)와 체크섬(깨졌는지 정도만 확인) 둘뿐이다. 그래서 빠르고 가볍다 — 연결 상태를 기억할 필요가 없으니 서버가 수많은 클라이언트를 가뿐히 상대하고, 첫 패킷부터 왕복 지연 없이 바로 나간다.

그래서 무엇이 다른가 — TCP vs UDP

TCPUDP
연결핸드셰이크로 수립없음(그냥 보냄)
신뢰성재전송·순서·중복 제거없음(빠지면 그만)
제어흐름·혼잡 제어없음
무게상태·절차 많음가벼움·빠름
적합정확성이 중요(웹·파일·DB)지연이 중요(통화·게임·DNS)

선택 기준은 한 줄이다 — 한 바이트도 틀리면 안 되는가(TCP), 아니면 늦느니 차라리 빠지는 게 나은가(UDP). 웹페이지·파일 전송·DB는 TCP, 실시간 음성·영상·온라인 게임은 UDP가 어울린다. DNS도 (짧은 질의·응답이라) 기본은 UDP를 쓴다(다음 편).

신뢰성이 필요하면? 앱이 골라서 얹는다

흥미로운 건 — UDP를 쓴다고 신뢰성을 영영 포기하는 게 아니라, 필요한 만큼만 애플리케이션이 직접 얹을 수 있다는 점이다. 게임은 "캐릭터 위치는 좀 빠져도 되지만(최신만 중요), 아이템 획득은 반드시 도착해야 한다"처럼 — 메시지마다 신뢰 수준을 다르게 설계한다. 이 발상을 표준으로 끌어올린 게 **QUIC**다. UDP 위에 신뢰성·순서·혼잡 제어·*암호화(TLS)*까지 얹되, TCP의 약점(연결마다 핸드셰이크 RTT, 한 스트림이 막히면 다 막히는 head-of-line 블로킹)을 개선했다 — *HTTP/3*가 바로 이 QUIC 위에서 돈다(HTTP 편에서 다시).

실무에서는 — "왜 게임/통화는 끊겨도 멈추진 않을까"의 답이 UDP다. 재전송으로 멈추기보다 빠진 채 진행하는 설계다. 반대로 DNS가 가끔 TCP로 떨어지는 경우도 있는데(응답이 UDP 한 패킷에 안 담길 만큼 클 때), 이는 "기본은 가볍게(UDP), 필요하면 무겁게(TCP)"의 전형이다. 그리고 *방화벽·NAT*가 UDP를 까다롭게 다루기도 해(상태가 없어 매핑이 빨리 만료), 실시간 서비스는 keepalive 패킷으로 매핑을 살려 두기도 한다.

실제로 눈으로 보기

핸드셰이크가 없다는 것을 확인한다

sudo tcpdump -i any -nn port 53 &
dig +short example.com @8.8.8.8

# 10.0.2.15.41022 > 8.8.8.8.53: 12345+ A? example.com. (29)
# 8.8.8.8.53 > 10.0.2.15.41022: 12345 1/0/0 A 93.184.216.34 (45)

패킷이 딱 두 개다. TCP 였다면 앞에 SYN·SYN+ACK·ACK 셋이 더 붙고 뒤에 종료 넷이 붙어 아홉 개가 된다. 질의 하나에 왕복 하나 — DNSUDP 를 쓰는 이유가 이 숫자에 있다.

받는 쪽이 없어도 보내진다

nc -u -l 9999 &                  # 서버를 띄우고
echo "hello" | nc -u -w1 127.0.0.1 9999    # 도착한다

kill %1                           # 서버를 죽인 뒤
echo "hello" | nc -u -w1 127.0.0.1 9999    # 오류 없이 그냥 끝난다

보내는 쪽은 실패했다는 것을 모른다. TCP 라면 연결 자체가 거부되지만, UDP 는 던지고 끝이다. 상대가 죽었는지 알려면 애플리케이션이 직접 확인해야 한다.

netstat -su | grep -A3 Udp:
#     412 packets received
#     18 packet receive errors      ← 버퍼가 넘쳐 버려진 것
#     0 packets to unknown port

receive errors가 오르면 수신 버퍼가 넘쳐 조용히 버려지고 있다는 뜻이다. TCP 라면 흐름 제어가 보내는 쪽을 늦췄겠지만 UDP 에는 그 장치가 없다.

크기를 잘못 잡으면 잘린다

dig +notcp +bufsize=512 TXT big.example.com
# ;; Truncated, retrying in TCP mode.       ← TC 비트가 서 있다

UDP 응답이 버퍼보다 크면 잘라서 보내고 "잘렸다" 표시만 준다. 그래서 DNS 는 이 경우 TCP 로 다시 물어본다 — "UDP 는 신뢰성이 없으니 앱이 알아서" 의 실제 사례다.

손실을 앱이 어떻게 다루나

DNS      → 잘리면 TCP 로 재시도, 응답 없으면 다른 서버로
실시간 음성 → 재전송하지 않는다. 늦게 온 음성은 쓸모가 없다
게임      → 최신 상태만 의미가 있어 옛 패킷은 버린다
QUIC     → UDP 위에 순서·재전송·혼잡 제어를 직접 구현했다

QUIC 이 특히 흥미롭다. TCP 의 기능이 필요했지만 커널에 박힌 TCP 는 고치기 어려웠다. 그래서 가장 아무것도 안 하는 UDP 위에 필요한 것만 직접 얹었다 — UDP 의 값어치가 "기능이 많아서" 가 아니라 "방해하지 않아서" 라는 점을 보여 준다.

정리하면, UDP는 IP의 best-effort를 거의 그대로 노출하는 가벼운 전송 계층이다 — 포트와 체크섬만 더하고, 핸드셰이크·재전송·제어는 없다. 그래서 빠르고 가벼워 지연이 중요한 실시간(통화·게임·DNS)에 어울린다. 신뢰성이 필요하면 앱이 필요한 만큼 직접 얹고, 그걸 표준화한 QUIC가 *HTTP/3*의 토대가 된다. 정확성이 중요하면 TCP, 지연이 중요하면 UDP — 전송 계층의 두 갈래다.

TCP — 핸드셰이크·신뢰성·흐름/혼잡 제어DNS — 이름 해석·재귀/반복·캐싱·TTL