TCP vs UDP 판단 기준
TCP는 신뢰성을 보장하고, UDP는 보장하지 않습니다. 그렇다면 TCP가 항상 더 좋은 선택일까요? 그렇지 않습니다. 신뢰성이라는 강력한 보장에는 반드시 비용이 따릅니다.
정확히 말하면 TCP는 연결 기반의 신뢰성 있는 순서 보장 바이트 스트림을 제공합니다. 손실 재전송, 순서 재조립, 흐름 제어, 혼잡 제어를 전송 계층이 맡습니다. UDP는 최소 기능의 데이터그램입니다. 연결 설정이 없고 메시지 경계는 유지되지만, 전달 여부·순서·중복 방지는 애플리케이션이 필요에 따라 직접 처리해야 합니다.
사용 사례별 비교
구체적인 사용 사례를 통해 어떤 프로토콜이 적합한지 살펴보겠습니다.
| 사용 사례 | 프로토콜 | 이유 |
|---|---|---|
| 웹 (HTTP/1.1, HTTP/2) | TCP | HTML/CSS/JS 한 바이트라도 빠지면 렌더링 실패 |
| 웹 (HTTP/3) | QUIC (UDP 기반) | UDP 위 다중 스트림 + TLS 1.3, 연결 단위 HOL 완화 |
| 온라인 게임 (위치 동기화) | UDP | 0.5초 전 위치보다 최신 위치가 중요 |
| 온라인 게임 (아이템 구매) | TCP | 거래 데이터는 손실 불가 |
| 영상 스트리밍 (실시간) | UDP (RTP) | 끊김 > 지연, 이전 프레임 재전송 무의미 |
| 영상 스트리밍 (VOD) | TCP (HLS/DASH) | 버퍼링 가능, 완전한 파일 전달 필요 |
| DNS 조회 | UDP | 작은 패킷, 연결 오버헤드 > 데이터 |
| DNS 영역 전송 | TCP | 대량 데이터, 완전성 필수 |
| VoIP (음성 통화) | UDP (RTP) | 실시간성이 핵심, 과거 음성 재전송 무의미 |
| 파일 전송 (FTP, SFTP) | TCP | 1바이트 누락 = 파일 손상 |
| IoT 센서 데이터 | UDP (CoAP) | 저전력, 소량 데이터, 최신 값만 의미 |
| 이메일 (SMTP) | TCP | 메일 내용 완전 전달 필수 |
| NTP (시간 동기화) | UDP | 작은 패킷, 연결 불필요 |
| DHCP | UDP | 브로드캐스트 필요, IP 할당 전이므로 TCP 불가 |
DNS는 “조회는 UDP, 영역 전송은 TCP” 정도로 외우면 입문 단계에서는 편하지만, 실제 DNS 구현은 TCP 지원도 중요합니다. 응답이 잘리거나 DNSSEC처럼 응답 크기가 커지는 경우, 또는 영역 전송처럼 완전성이 필요한 경우 TCP가 사용됩니다.
지연 민감도 vs 신뢰성 트레이드오프
패턴이 보이시나요? 핵심은 지연 민감도(Latency Sensitivity)와 신뢰성 요구 수준의 트레이드오프입니다.
TCP의 재전송은 신뢰성을 보장하지만, 재전송으로 인한 지연이 발생합니다. 더 큰 문제는 Head-of-Line(HOL) Blocking입니다. TCP는 하나의 순서 있는 바이트 스트림이기 때문에, 중간에 하나의 세그먼트가 손실되면 그 뒤에 도착한 데이터도 애플리케이션에 바로 전달되지 못하고 대기해야 합니다.
실시간 서비스에서는 이 HOL Blocking이 치명적입니다. 한 프레임의 손실 때문에 이후의 모든 프레임이 지연되는 것보다, 손실된 프레임을 건너뛰고 다음 프레임을 표시하는 것이 사용자 경험에 훨씬 유리합니다.
판단 기준을 정리하면 이렇습니다.
-
데이터 완전성이 필수이면 → TCP
-
실시간성이 완전성보다 중요하면 → UDP
-
작은 요청-응답이고 재시도가 단순하면 → UDP (연결 오버헤드 제거)
-
순서와 완전성이 필요한 스트림이면 → TCP 또는 QUIC
-
프레임 단위 최신성이 더 중요하면 → UDP 또는 애플리케이션 레벨 UDP
-
방화벽·프록시·기업망에서 UDP가 제한될 수 있으면 → TCP/HTTPS 기반 fallback도 함께 설계
애플리케이션 레벨에서 신뢰성 보완하기
UDP를 선택했지만 어느 정도의 신뢰성이 필요한 경우가 많습니다. 100% 신뢰성은 필요 없지만, 완전히 무시할 수도 없는 상황입니다.
이때는 애플리케이션 레벨에서 필요한 만큼만 신뢰성을 구현합니다.
import socket
import struct
import time
import threading
class ReliableUDP:
"""부분적 신뢰성을 가진 UDP 구현"""
# 메시지 타입
UNRELIABLE = 0 # ACK 불필요 (위치 업데이트 등)
RELIABLE = 1 # ACK 필요 (아이템 획득 등)
ACK = 2 # 확인 응답
def __init__(self, port):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.bind(("0.0.0.0", port))
self.seq = 0
self.pending_acks = {} # seq → (data, addr, timestamp, retries)
self.max_retries = 3
self.retry_interval = 0.5 # 500ms
def send_unreliable(self, data, addr):
"""신뢰성 불필요 메시지 (위치, 이펙트 등)"""
# 헤더: [type(1B)][seq(4B)]
packet = struct.pack("!BI", self.UNRELIABLE, self.seq) + data
self.sock.sendto(packet, addr)
self.seq += 1
def send_reliable(self, data, addr):
"""신뢰성 필요 메시지 (아이템, 채팅 등)"""
seq = self.seq
packet = struct.pack("!BI", self.RELIABLE, seq) + data
self.sock.sendto(packet, addr)
self.pending_acks[seq] = (packet, addr, time.time(), 0)
self.seq += 1
return seq
def send_ack(self, seq, addr):
"""ACK 전송"""
packet = struct.pack("!BI", self.ACK, seq)
self.sock.sendto(packet, addr)
def check_retransmits(self):
"""재전송 확인 (타이머 스레드에서 호출)"""
now = time.time()
to_remove = []
for seq, (packet, addr, timestamp, retries) in self.pending_acks.items():
if now - timestamp > self.retry_interval:
if retries >= self.max_retries:
print(f"[WARN] seq={seq} 전달 실패 (재시도 {retries}회)")
to_remove.append(seq)
else:
self.sock.sendto(packet, addr)
self.pending_acks[seq] = (packet, addr, now, retries + 1)
print(f"[RETRY] seq={seq} 재전송 ({retries + 1}회)")
for seq in to_remove:
del self.pending_acks[seq]
def receive(self):
"""메시지 수신 및 처리"""
data, addr = self.sock.recvfrom(65535)
msg_type, seq = struct.unpack("!BI", data[:5])
payload = data[5:]
if msg_type == self.ACK:
if seq in self.pending_acks:
del self.pending_acks[seq]
return None, None, addr
if msg_type == self.RELIABLE:
self.send_ack(seq, addr)
return msg_type, payload, addr
# 사용 예시:
# server = ReliableUDP(9000)
#
# 위치 업데이트 (손실 허용):
# server.send_unreliable(position_data, player_addr)
#
# 아이템 획득 (손실 불가):
# server.send_reliable(item_data, player_addr)게임에서 이런 접근이 일반적입니다. 위치 업데이트는 ACK 없이 보내고(어차피 곧 새 위치가 올 것이므로), 스킬 사용이나 아이템 획득 같은 이벤트는 ACK를 요구합니다.
이런 부분적 신뢰성을 제공하는 라이브러리(ENet, RakNet 등)도 존재합니다. TCP의 전부 아니면 전무가 아닌, 상황에 맞는 유연한 처리가 가능합니다.
TCP vs UDP 면접 정리
| 질문 | 핵심 답변 |
|---|---|
| TCP와 UDP의 차이는? | TCP는 연결/신뢰/순서 보장, UDP는 비연결/비신뢰/최소 오버헤드 |
| UDP를 왜 사용하나요? | 실시간성 > 신뢰성인 경우 (게임, VoIP, 스트리밍) |
| TCP의 HOL Blocking이란? | 하나의 손실이 전체 스트림을 지연시키는 현상 |
| 게임에서 TCP/UDP 동시 사용? | 위치/이펙트는 UDP, 로그인/아이템은 TCP |
| DNS가 UDP를 쓰는 이유? | 작은 질의-응답, 연결 오버헤드가 데이터보다 큼 |
| UDP에서 신뢰성 구현? | 애플리케이션 레벨 ACK/재전송 (부분적 신뢰성) |
| QUIC은 TCP인가 UDP인가? | UDP 위에서 TLS 1.3, 다중 스트림, 흐름/혼잡 제어를 제공하는 전송 프로토콜 |
다음 절에서는 UDP 기반이면서도 TCP의 장점까지 통합한 차세대 프로토콜 QUIC을 살펴보겠습니다.