UDP의 특성
TCP가 신뢰성, 순서 보장, 흐름 제어, 혼잡 제어 같은 기능을 전송 계층에서 제공한다면, UDP(User Datagram Protocol)는 훨씬 얇은 인터페이스를 제공합니다. 포트 번호로 애플리케이션을 구분하고, 데이터그램 단위로 보내며, 기본 오류 검출을 위한 체크섬을 둘 뿐입니다.
그 대신 UDP 자체는 연결 설정, 도착 확인, 재전송, 순서 정렬, 흐름 제어, 혼잡 제어를 맡지 않습니다. 필요한 기능은 DNS, RTP, QUIC 같은 상위 프로토콜이나 애플리케이션이 직접 설계해야 합니다.
비연결성, 비신뢰성, 최소 오버헤드
UDP를 이해할 때 핵심은 "빠른 프로토콜"이라고 외우는 것이 아니라, 전송 계층이 맡는 책임이 작다고 보는 것입니다.
비연결성(Connectionless): TCP의 3-way handshake 같은 연결 설정 과정이 없습니다. 송신자는 목적지 IP와 포트를 지정해 데이터그램을 바로 보냅니다. 다만 이것은 "상대가 준비되었는지 확인했다"는 뜻이 아닙니다.
비신뢰성(Unreliable): 손실된 데이터그램을 UDP가 재전송하지 않고, 순서가 뒤바뀌어도 정렬하지 않으며, 중복 도착을 제거하지도 않습니다. 네트워크는 최선을 다해 전달하지만, 결과 보장은 애플리케이션의 몫입니다.
최소 오버헤드(Minimal Overhead): TCP 헤더는 최소 20바이트이고 옵션이 붙을 수 있지만, UDP 헤더는 8바이트 고정입니다. 연결 관리, 순서 번호, 확인 번호, 윈도우 크기, 플래그가 없기 때문입니다.
UDP 헤더 구조
UDP 헤더가 얼마나 단순한지 살펴보면, UDP의 철학을 체감할 수 있습니다.
| 필드 | 크기 | 설명 |
|---|---|---|
| Source Port | 2바이트 | 출발지 포트. 응답이 필요 없는 경우 0으로 둘 수 있음 |
| Destination Port | 2바이트 | 목적지 포트. 수신 호스트의 애플리케이션 프로세스 식별 |
| Length | 2바이트 | UDP 헤더 8바이트와 데이터까지 포함한 UDP 데이터그램 전체 길이 |
| Checksum | 2바이트 | 오류 검출용 값. IPv4에서는 0으로 비활성화 가능, IPv6 일반 UDP에서는 필요 |
이것이 UDP 헤더의 전부입니다. TCP 헤더에 있던 순서 번호, 확인 번호, 윈도우 크기, 플래그는 없습니다. UDP는 전송 계층의 기본 역할인 포트 기반 다중화와 데이터그램 전달만 얇게 제공하고, 나머지 정책은 상위 계층에 남깁니다.
메시지 경계 보존
TCP와 UDP의 중요한 차이 하나는 메시지 경계입니다. TCP는 바이트 스트림이므로 송신자가 여러 번 send()한 경계가 수신자의 recv() 경계와 일치하지 않을 수 있습니다.
반면 UDP는 하나의 sendto()가 하나의 데이터그램을 만들고, 그 데이터그램이 도착하면 수신 측은 데이터그램 단위로 받습니다. 단, 수신 버퍼가 데이터그램보다 작으면 남는 부분이 잘릴 수 있으므로 버퍼 크기도 함께 설계해야 합니다.
체크섬의 역할
UDP는 신뢰성을 보장하지 않지만, 체크섬을 통해 최소한의 무결성 검증은 제공합니다.
UDP 체크섬은 UDP 헤더와 데이터뿐 아니라, IP 계층에서 가져온 출발지 주소, 목적지 주소, 프로토콜 번호, UDP 길이를 포함한 pseudo header까지 함께 계산합니다. 실제로 전송되는 UDP 헤더에 pseudo header가 들어가는 것은 아니고, 체크섬 계산에만 임시로 포함됩니다.
계산 방식은 16비트 단위의 1의 보수 합을 기반으로 합니다. 수신 측도 같은 범위로 계산해 값이 맞는지 확인합니다. 하지만 체크섬이 모든 오류를 완벽히 증명하는 것은 아니며, 오류를 검출하려는 장치에 가깝습니다.
중요한 점은 체크섬이 오류를 수정하지 않는다는 것입니다. 값이 맞지 않으면 데이터그램을 폐기할 수 있지만, UDP가 재전송을 요청하지는 않습니다. IPv4에서 UDP 체크섬은 0으로 비활성화할 수 있고, IPv6의 일반 UDP에서는 체크섬이 필요합니다. 일부 터널링 용도에는 예외가 있지만, 일반 애플리케이션 설계에서는 IPv6 UDP 체크섬을 필수로 보면 됩니다.
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)
text = data.decode(errors="replace")
print(f"수신: {text} from {client_addr}")
server.sendto(b"Echo: " + data, client_addr)
# TCP와 달리: 매번 상대 주소를 명시해야 함
# -> connect()하지 않은 UDP 소켓은 기본 상대가 없음
# === UDP 클라이언트 ===
def udp_client():
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# UDP connect()는 선택 사항이며, TCP처럼 핸드셰이크를 만들지 않음
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() 연결별 close 없음
# socket.close() UDP도 소켓 리소스는 닫아야 함UDP의 최대 크기 제한
UDP 데이터그램의 이론적 최대 크기와 실제 제한을 이해해야 합니다.
| 기준 | 크기 | 설명 |
|---|---|---|
| UDP Length 필드 | 최대 65,535B | UDP 헤더와 데이터를 합친 길이 |
| UDP 자체 페이로드 | 최대 65,527B | 65,535B에서 UDP 헤더 8B를 제외 |
| IPv4 기본 헤더 기준 페이로드 | 최대 65,507B | IPv4 Total Length 65,535B에서 IP 20B와 UDP 8B 제외 |
| Ethernet MTU 1500 기준 | 보통 1,472B | IPv4 20B와 UDP 8B를 제외한 단편화 없는 UDP 데이터 크기 |
| IPv6 최소 MTU 1280 기준 | 보통 1,232B | IPv6 기본 헤더 40B와 UDP 8B를 제외한 값 |
| 실무 설계 | 경로 MTU에 맞춰 조정 | VPN, 터널, IPv6, 무선망에서는 더 작게 잡아야 할 수 있음 |
이론상 큰 UDP 데이터그램을 만들 수는 있지만, 경로 MTU보다 크면 IP 단편화가 발생할 수 있습니다. 조각 중 하나만 사라져도 전체 데이터그램을 복원할 수 없으므로, 실무에서는 경로 MTU 탐색이나 애플리케이션 레벨 분할을 함께 고려합니다.
이처럼 UDP는 극도로 가볍지만 전송 계층으로서의 최소 기능만 갖고 있습니다. 다음 절에서는 이런 UDP가 TCP 대신 선택되는 구체적인 상황과 판단 기준을 살펴보겠습니다.