TCP vs UDP 판단 기준
TCP는 신뢰성을 보장하고, UDP는 보장하지 않습니다. 그렇다면 TCP가 항상 더 좋은 선택일까요? 그렇지 않습니다. 신뢰성이라는 강력한 보장에는 반드시 비용이 따릅니다.
TCP로 1바이트 데이터를 보내려면
1. 연결 수립: SYN → SYN-ACK → ACK (3패킷, 1 RTT)
2. 데이터 전송: Data → ACK (2패킷)
3. 연결 종료: FIN → ACK → FIN → ACK (4패킷, 2 RTT)
총 비용: 9패킷, 3+ RTT
순수 데이터: 1바이트
DNS 쿼리처럼 "50바이트 질문, 100바이트 응답"인 경우
TCP 오버헤드가 데이터보다 훨씬 크다!사용 사례별 비교
구체적인 사용 사례를 통해 어떤 프로토콜이 적합한지 살펴보겠습니다.
| 사용 사례 | 프로토콜 | 이유 |
|---|---|---|
| 웹 (HTTP/1.1, HTTP/2) | TCP | HTML/CSS/JS 한 바이트라도 빠지면 렌더링 실패 |
| 웹 (HTTP/3) | QUIC (UDP 기반) | TCP HOL Blocking 해결 + 0-RTT 재연결 |
| 온라인 게임 (위치 동기화) | 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 불가 |
게임 서버
UDP (실시간)
┌────────────┐
│ 위치 동기화│
│ 총알 궤적 │
│ 이펙트 │
│ 음성 채팅 │
└────────────┘
특성: 손실 허용, 낮은 지연 필요
TCP (중요 데이터)
┌────────────┐
│ 로그인 │
│ 아이템 거래│
│ 채팅 │
│ 매치 결과 │
└────────────┘
특성: 손실 불가, 순서 + 완전성 필수지연 민감도 vs 신뢰성 트레이드오프
패턴이 보이시나요? 핵심은 지연 민감도(Latency Sensitivity)와 신뢰성 요구 수준의 트레이드오프입니다.
신뢰성 요구
낮음 ◄─────────────► 높음
┌─────────────┬─────────────┐
높음 │ UDP │ QUIC │
│ (게임,VoIP) │ (HTTP/3) │
지연 │ │ │
민감도 ├─────────────┼─────────────┤
│ UDP │ TCP │
낮음 │ (DNS,IoT) │ (HTTP,FTP) │
│ │ (이메일) │
└─────────────┴─────────────┘TCP의 재전송은 신뢰성을 보장하지만, 재전송으로 인한 지연이 발생합니다. 더 큰 문제는 Head-of-Line(HOL) Blocking입니다. TCP는 순서를 보장하기 때문에, 중간에 하나의 패킷이 손실되면 그 뒤의 데이터가 모두 수신 버퍼에서 대기해야 합니다.
TCP 스트림 (HTTP/2 멀티플렉싱)
패킷: [A1][B1][A2][B2][A3][B3]
↑
A2 손실 (재전송 대기)
수신 버퍼: [A1][ ][B1][B2][A3][B3]
↑
A2 도착 대기 중
→ B1, B2, A3, B3 모두 애플리케이션에 전달 못함!
→ 스트림 B는 A와 무관한데도 블로킹!
UDP 기반 (QUIC)
스트림 A: [A1][ ][A3] → A2 재전송 대기는 A만 영향
스트림 B: [B1][B2][B3] → 즉시 애플리케이션에 전달! ✓실시간 서비스에서는 이 HOL Blocking이 치명적입니다. 한 프레임의 손실 때문에 이후의 모든 프레임이 지연되는 것보다, 손실된 프레임을 건너뛰고 다음 프레임을 표시하는 것이 사용자 경험에 훨씬 유리합니다.
판단 기준을 정리하면 이렇습니다.
-
데이터 완전성이 필수이면 → TCP
-
실시간성이 완전성보다 중요하면 → UDP
-
작은 요청-응답 패턴이면 → UDP (연결 오버헤드 제거)
-
양방향 스트리밍이면 → TCP 또는 애플리케이션 레벨 UDP
애플리케이션 레벨에서 신뢰성 보완하기
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 위에 TCP+TLS를 사용자 공간에서 구현한 것 |
다음 절에서는 UDP 기반이면서도 TCP의 장점까지 통합한 차세대 프로토콜 QUIC을 살펴보겠습니다.