UDP의 특성
TCP가 신뢰성, 순서 보장, 흐름 제어, 혼잡 제어 등 많은 기능을 제공한다면, UDP(User Datagram Protocol)는 그런 것을 일절 하지 않습니다. 연결도 맺지 않고, 데이터가 도착했는지 확인하지도 않으며, 순서가 뒤바뀌어도 신경 쓰지 않습니다.
그럼 왜 사용하나요?라는 의문이 들 수밖에 없습니다. 그 답은 이 장에서 하나씩 풀어가겠습니다.
비연결성, 비신뢰성, 최소 오버헤드
UDP의 세 가지 핵심 특성은 TCP와 정확히 반대입니다.
┌─────────────────┬──────────────────┬───────────────────┐
│ 특성 │ TCP │ UDP │
├─────────────────┼──────────────────┼───────────────────┤
│ 연결 │ 3-way handshake │ 없음 (바로 전송) │
│ 신뢰성 │ ACK + 재전송 │없음 (Best Effort) │
│ 순서 보장 │ Sequence Number │ 없음 │
│ 흐름 제어 │ rwnd 기반 │ 없음 │
│ 혼잡 제어 │ cwnd 기반 │ 없음 │
│ 헤더 크기 │ 20~60 바이트 │ 8 바이트 │
│ 전송 단위 │ 바이트 스트림 │데이터그램 (메시지)│
│ 메시지 경계 │ 없음 │ 보존 │
│ 1:N 통신 │ 불가 (1:1만) │ 가능 │
└─────────────────┴──────────────────┴───────────────────┘비연결성(Connectionless): 3-way handshake 같은 연결 설정 과정이 없습니다. 보내고 싶을 때 바로 보냅니다. 전화를 걸어 상대방이 받기를 기다리는 것이 아니라, 편지를 써서 우체통에 넣는 것과 같습니다. 상대방이 받았는지는 알 수 없습니다.
비신뢰성(Unreliable): 패킷이 손실되어도 재전송하지 않습니다. 순서가 뒤바뀌어도 정렬하지 않습니다. 중복 패킷이 도착해도 걸러내지 않습니다. "최선을 다해" 보내되, 결과는 보장하지 않는 Best Effort 방식입니다.
최소 오버헤드(Minimal Overhead): TCP 헤더는 최소 20바이트(옵션 포함 시 최대 60바이트)이지만, UDP 헤더는 딱 8바이트입니다. 연결 관리, 순서 번호, 윈도우 크기 등이 모두 없기 때문입니다. 이 작은 오버헤드가 UDP의 강점입니다.
TCP 패킷 (1바이트 데이터 전송)
[Ethernet 14B][IP 20B][TCP 20B][Data 1B][Ethernet FCS 4B]
= 59바이트 → 데이터 효율: 1/59 = 1.7%
UDP 패킷 (1바이트 데이터 전송)
[Ethernet 14B][IP 20B][UDP 8B][Data 1B][Ethernet FCS 4B]
= 47바이트 → 데이터 효율: 1/47 = 2.1%
1000바이트 데이터
TCP: 1000/1054 = 94.9%
UDP: 1000/1042 = 96.0%
→ 작은 패킷일수록 TCP의 헤더 부담이 크고
→ 큰 패킷에서는 차이가 줄어듦
→ 하지만 TCP의 진짜 오버헤드는 헤더가 아니라
handshake, ACK, 재전송 등의 "동작" 오버헤드UDP 헤더 구조
UDP 헤더가 얼마나 단순한지 살펴보면, UDP의 철학을 체감할 수 있습니다.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ Source Port │ Destination Port │
├───────────────────────────────┼────────────────────────────────┤
│ Length │ Checksum │
├───────────────────────────────┴────────────────────────────────┤
│ Data (페이로드) │
└────────────────────────────────────────────────────────────────┘
vs TCP 헤더 (최소 20바이트 + 옵션)
Source Port, Destination Port,
Sequence Number (4B), ← UDP에 없음
Acknowledgment Number (4B), ← UDP에 없음
Data Offset, Flags, ← UDP에 없음
Window Size (2B), ← UDP에 없음
Checksum, Urgent Pointer, ← Checksum만 있음
Options (가변) ← UDP에 없음| 필드 | 크기 | 설명 |
|---|---|---|
| Source Port | 2바이트 | 출발지 포트. 선택적이며 응답 불필요 시 0 가능 |
| Destination Port | 2바이트 | 목적지 포트. 수신 프로세스 식별 |
| Length | 2바이트 | UDP 헤더(8B) + 페이로드 전체 길이 |
| Checksum | 2바이트 | 데이터 무결성 검증 (IPv4 선택, IPv6 필수) |
이것이 전부입니다. TCP 헤더에 있던 순서 번호, 확인 번호, 윈도우 크기, 플래그 등이 전부 빠져 있습니다. UDP는 전송 계층이 추가할 수 있는 최소한의 기능만 제공하고, 나머지는 애플리케이션에 맡기는 것입니다.
메시지 경계 보존
TCP와 UDP의 중요한 차이 하나를 짚어 두겠습니다. UDP는 메시지 경계를 보존합니다.
TCP (바이트 스트림)
send("Hello") → 5바이트
send("World") → 5바이트
recv() → "HelloWor" (8바이트) ← 경계 없이 연속된 바이트
recv() → "ld" (2바이트)
UDP (데이터그램)
sendto("Hello") → 데이터그램 1
sendto("World") → 데이터그램 2
recvfrom() → "Hello" (5바이트) ← 데이터그램 1 완전히
recvfrom() → "World" (5바이트) ← 데이터그램 2 완전히
→ UDP는 한 번의 send가 한 번의 recv에 대응
→ 메시지 경계를 애플리케이션이 따로 처리할 필요 없음
→ 단, UDP 데이터그램 크기 > 수신 버퍼 → 초과분 버려짐!체크섬의 역할
UDP는 신뢰성을 보장하지 않지만, 체크섬을 통해 최소한의 무결성 검증은 제공합니다.
체크섬은 헤더와 페이로드를 16비트 단위로 나누어 합산한 값의 1의 보수입니다. 수신 측이 같은 연산을 수행하여 값이 일치하면 데이터가 전송 중 손상되지 않았음을 확인합니다.
중요한 점은, 체크섬이 오류를 감지할 뿐 오류를 수정하지는 않는다는 것입니다. 체크섬이 일치하지 않으면 패킷을 폐기합니다. 재전송을 요청하지 않습니다.
IPv4에서 UDP 체크섬은 선택 사항(0으로 설정 가능)이지만, IPv6에서는 필수입니다. IPv6는 IP 헤더 자체에서 체크섬을 제거했기 때문에, 전송 계층에서 반드시 무결성 검증을 수행해야 합니다.
체크섬 계산에는 UDP 헤더만이 아니라 "Pseudo Header"도 포함됩니다.
Pseudo Header
┌─────────────────────────────────┐
│ Source IP Address (4B) │ ← IP 헤더에서 가져옴
├─────────────────────────────────┤
│ Destination IP Address (4B) │ ← IP 헤더에서 가져옴
├────────┬────────────────────────┤
│Zero(1B)│Protocol(1B)│UDP Len(2B)│
└────────┴────────────────────────┘
이렇게 하는 이유
IP 헤더의 출발지/목적지가 변조되어도 체크섬으로 감지 가능
→ "잘못된 호스트에 배달된 패킷"을 필터링UDP 소켓 프로그래밍
TCP와의 차이점을 코드로 확인해 봅시다.
import socket
import time
# === UDP 서버 (에코 서버) ===
def udp_server():
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # DGRAM = UDP
server.bind(("0.0.0.0", 9999))
print("UDP 서버 시작 (포트 9999)")
# TCP와 달리: listen(), accept() 없음
# → 연결 수립 과정이 없으므로
while True:
data, client_addr = server.recvfrom(65535)
print(f"수신: {data.decode()} from {client_addr}")
server.sendto(f"Echo: {data.decode()}".encode(), client_addr)
# TCP와 달리: 매번 상대 주소를 명시해야 함
# → "연결"이 없으므로 누구에게 보내는지 항상 알려줘야
# === UDP 클라이언트 ===
def udp_client():
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# TCP와 달리: connect() 없어도 됨 (선택적)
server_addr = ("127.0.0.1", 9999)
# 메시지 경계 보존 확인
messages = ["Hello", "World", "UDP"]
for msg in messages:
client.sendto(msg.encode(), server_addr)
for _ in messages:
data, addr = client.recvfrom(65535)
print(f"응답: {data.decode()}")
client.close()
# TCP vs UDP 소켓 코드 비교:
#
# TCP: UDP:
# socket(SOCK_STREAM) socket(SOCK_DGRAM)
# server.listen(5) (없음)
# conn, addr = server.accept() (없음)
# client.connect(addr) (선택적)
# conn.send(data) sendto(data, addr)
# conn.recv(1024) recvfrom(65535)
# conn.close() (없음 - 연결이 없으므로)UDP의 최대 크기 제한
UDP 데이터그램의 이론적 최대 크기와 실제 제한을 이해해야 합니다.
이론적 최대
UDP Length 필드: 16비트 → 최대 65,535바이트
UDP 헤더: 8바이트
→ 최대 페이로드: 65,527바이트
IP 레벨
IP Total Length: 16비트 → 최대 65,535바이트
IP 헤더: 20바이트, UDP 헤더: 8바이트
→ 최대 UDP 페이로드: 65,507바이트
실제 제한 (MTU 기반)
이더넷 MTU: 1500바이트
IP 헤더: 20바이트, UDP 헤더: 8바이트
→ MTU 내 최대 페이로드: 1,472바이트
UDP 페이로드 > 1,472바이트
→ IP 단편화(Fragmentation) 발생
→ 하나의 UDP 데이터그램이 여러 IP 패킷으로 분할
→ 단편 하나라도 손실 → 전체 데이터그램 폐기!
→ 다시 보내지 않음 (UDP는 재전송 없음)
실무 권장
DNS: 512바이트 (전통), 4096바이트 (EDNS0)
게임: 500~1200바이트
VoIP: 160~320바이트 (20ms 오디오 프레임)
일반: 1,472바이트 이하 (단편화 방지)| 수준 | 최대 크기 | 설명 |
|---|---|---|
| UDP Length 필드 | 65,535B | 이론적 최대 |
| IP 페이로드 | 65,507B | IP 헤더 제외 |
| MTU 기반 (단편화 없이) | 1,472B | 이더넷 MTU - IP(20) - UDP(8) |
| 실무 권장 | 512~1,400B | 단편화 + 경로 MTU 고려 |
이처럼 UDP는 극도로 가벼우면서도 전송 계층으로서의 최소 기능(포트 기반 다중화, 기본 오류 감지)은 갖추고 있습니다. 다음 절에서는 이런 UDP가 TCP 대신 선택되는 구체적인 상황과 판단 기준을 살펴보겠습니다.