TCP vs UDP 판단 기준
전달 신뢰성·순서·지연·메시지 경계의 비용을 비교하고 서비스 요구에 따라 TCP와 UDP 및 보완 전략을 선택합니다.
TCP는 손실 복구와 순서 있는 바이트 전달을 제공하고, UDP 자체는 이를 제공하지 않습니다. 연결이 실패해도 전달 완료를 보장한다는 뜻은 아닙니다.
그렇다면 TCP가 항상 더 좋은 선택일까요?
그렇지 않습니다.
신뢰성이라는 강력한 보장에는 반드시 비용이 따릅니다.
정확히 말하면 TCP는 연결 기반의 신뢰성 있는 순서 보장 바이트 스트림을 제공합니다.
손실 재전송, 순서 재조립, 흐름 제어, 혼잡 제어를 전송 계층이 맡습니다.
UDP는 최소한의 기능만 제공하는 데이터그램 프로토콜입니다.
연결 설정이 없고 메시지 경계는 유지되지만, 전달 여부·순서·중복 방지는 애플리케이션이 필요에 따라 직접 처리해야 합니다.
사용 사례별 비교
구체적인 사용 사례를 통해 어떤 프로토콜이 적합한지 살펴보겠습니다.
| 사용 사례 | 선택 예와 이유 |
|---|---|
| 웹 (HTTP/1.1, HTTP/2) | 프로토콜: TCP; 이유: 문서·자산을 순서 있는 바이트 스트림으로 전달 |
| 웹 (HTTP/3) | 프로토콜: QUIC (UDP 기반); 이유: UDP 위 다중 스트림 + TLS 1.3, 연결 단위 HOL 완화 |
| 온라인 게임 (위치 동기화) | 프로토콜: UDP; 이유: 0.5초 전 위치보다 최신 위치가 중요 |
| 온라인 게임 (아이템 구매) | 프로토콜: TCP; 이유: 전송 오류와 거래 결과를 별도로 처리해야 함 |
| 영상 스트리밍 (실시간) | 프로토콜: UDP (RTP); 이유: 재생 기한 내 복구와 늦은 데이터 폐기를 선택 |
| 영상 스트리밍 (VOD) | 프로토콜: HTTP 기반 (TCP/QUIC); 이유: 버퍼링 가능, 완전한 파일 전달 필요 |
| DNS 조회 | 프로토콜: UDP; 이유: 작은 질의·응답에 흔함; TCP도 지원 |
| DNS 영역 전송 | 프로토콜: TCP; 이유: 대량 데이터, 완전성 필수 |
| VoIP (음성 통화) | 프로토콜: UDP (RTP); 이유: 재생 기한과 손실 복구 비용을 함께 고려 |
| 파일 전송 (FTP, SFTP) | 프로토콜: TCP; 이유: 1바이트 누락 = 파일 손상 |
| IoT 센서 데이터 | 프로토콜: UDP (CoAP); 이유: 작은 메시지; 확인형 메시지로 재전송도 가능 |
| 이메일 (SMTP) | 프로토콜: TCP; 이유: 메일 내용 완전 전달 필수 |
| NTP (시간 동기화) | 프로토콜: UDP; 이유: 작은 패킷, 연결 불필요 |
| DHCP | 프로토콜: UDP; 이유: DHCPv4 초기 탐색에서 미설정 주소와 브로드캐스트 사용 |
DNS는 “조회는 UDP, 영역 전송은 TCP” 정도로 외우면 입문 단계에서는 편하지만, 실제 DNS 구현은 TCP 지원도 중요합니다.
응답이 잘리거나 DNSSEC처럼 응답 크기가 커지는 경우, 또는 영역 전송처럼 완전성이 필요한 경우 TCP가 사용됩니다.
지연 민감도 vs 신뢰성 트레이드오프
패턴이 보이시나요?
핵심은 지연 민감도(Latency Sensitivity)와 신뢰성 요구 수준의 트레이드오프입니다.
TCP의 재전송은 손실 복구를 지원하지만, 복구를 기다리는 지연이 발생합니다.
더 큰 문제는 Head-of-Line(HOL) Blocking입니다.
TCP는 하나의 순서 있는 바이트 스트림이기 때문에, 중간에 하나의 세그먼트가 손실되면 그 뒤에 도착한 데이터도 애플리케이션에 바로 전달되지 못하고 대기해야 합니다.
재생·처리 기한이 짧은 실시간 서비스에서는 손실 복구 대기가 그 기한을 넘기는지 판단해야 합니다.
한 프레임의 손실 때문에 이후의 모든 프레임이 지연되는 것보다, 손실된 프레임을 건너뛰고 다음 프레임을 표시하는 것이 사용자 경험에 훨씬 유리합니다.
판단 기준을 정리하면 이렇습니다.
-
데이터 완전성이 필수이면 → TCP
-
실시간성이 완전성보다 중요하면 → UDP
-
작은 요청-응답이고 재시도가 단순하면 → UDP (연결 오버헤드 제거)
-
순서와 완전성이 필요한 스트림이면 → TCP 또는 QUIC
-
프레임 단위 최신성이 더 중요하면 → UDP 또는 애플리케이션 레벨 UDP
-
방화벽·프록시·기업망에서 UDP가 제한될 수 있으면 → TCP/HTTPS 기반 fallback도 함께 설계
애플리케이션 레벨에서 신뢰성 보완하기
UDP를 선택했지만 어느 정도의 신뢰성이 필요한 경우가 많습니다.
메시지별로 재시도 횟수와 도착 기한을 다르게 둘 수 있습니다.
이때는 애플리케이션 레벨에서 필요한 만큼만 신뢰성을 구현합니다.
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)재전송 예제가 처리하는 것과 남기는 것
순번 7을 받아 ACK하고 payload를 반환합니다. 뒤늦게 ACK를 받으면 대기 항목을 제거합니다. 각 단계의 값과 전환 이유를 비교합니다.
UDP · 학습용 재전송
ACK와 재시도만으로 중복 실행이나 업무 완료가 보장되지는 않는다
본문 코드는 ACK와 제한된 재전송의 구조를 보여줍니다. 패킷 재수신·상대 검증·동시 접근·기한·혼잡 제어의 책임은 별도로 남아 있습니다.
| 코드 경로 | 현재 동작 | 보장하지 않는 것 |
|---|---|---|
| send_reliable | 보낸 패킷을 pending_acks에 저장 | 최대 재시도 뒤에도 전달되지 않을 수 있음 |
| receive: 데이터 | RELIABLE이면 ACK를 보내고 payload 반환 | 같은 seq 재수신도 다시 반환; 중복 실행 방지 없음 |
| receive: ACK | 같은 seq의 대기 항목 삭제 | 보낸 상대 주소를 대조하지 않음; 업무 처리 완료 증거 아님 |
| 타이머 / 수신 루프 | 호출자가 check_retransmits와 receive를 실행해야 함 | dict 동시 변경 보호·수신 기한·혼잡 제어 없음 |
- send_reliable
- 현재 동작: 보낸 패킷을 pending_acks에 저장보장하지 않는 것: 최대 재시도 뒤에도 전달되지 않을 수 있음
- receive: 데이터
- 현재 동작: RELIABLE이면 ACK를 보내고 payload 반환보장하지 않는 것: 같은 seq 재수신도 다시 반환; 중복 실행 방지 없음
- receive: ACK
- 현재 동작: 같은 seq의 대기 항목 삭제보장하지 않는 것: 보낸 상대 주소를 대조하지 않음; 업무 처리 완료 증거 아님
- 타이머 / 수신 루프
- 현재 동작: 호출자가 check_retransmits와 receive를 실행해야 함보장하지 않는 것: dict 동시 변경 보호·수신 기한·혼잡 제어 없음
코드 검토에 따른 범위입니다. 패킷 손실·재전송·중복 처리를 실행해 관측한 결과는 아닙니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 첫 전달부터 ACK 수신까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | 같은 seq 7 데이터가 도착하여 ACK를 보내지만 첫 ACK만 유실됩니다. 송신 쪽 대기 항목이 남아 타이머 검사 뒤 같은 데이터를 재전송합니다. 원 코드는 중복 seq를 억제하지 않아 payload를 두 번 반환합니다. ACK는 업무 처리 완료를 뜻하지 않습니다. |
| 표의 값 | send_reliable — 현재 동작: 보낸 패킷을 pending_acks에 저장; 보장하지 않는 것: 최대 재시도 뒤에도 전달되지 않을 수 있음. receive: 데이터 — 현재 동작: RELIABLE이면 ACK를 보내고 payload 반환; 보장하지 않는 것: 같은 seq 재수신도 다시 반환; 중복 실행 방지 없음. receive: ACK — 현재 동작: 같은 seq의 대기 항목 삭제; 보장하지 않는 것: 보낸 상대 주소를 대조하지 않음; 업무 처리 완료 증거 아님. 타이머 / 수신 루프 — 현재 동작: 호출자가 check_retransmits와 receive를 실행해야 함; 보장하지 않는 것: dict 동시 변경 보호·수신 기한·혼잡 제어 없음. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 전달 순번 7을 받아 ACK하고 payload를 반환합니다. |
|
|
| ACK 유실 첫 ACK가 유실돼 송신 대기 항목이 남습니다. |
|
|
| 재시도 검사 호출자가 재시도 간격 초과를 검사합니다. |
|
|
| 중복 수신 예제 코드는 순번 중복을 억제하지 않습니다. |
|
|
| ACK 수신 뒤늦게 ACK를 받으면 대기 항목을 제거합니다. |
|
|
재전송 예제가 처리하는 것과 남기는 것
순번 7을 받아 ACK하고 payload를 반환합니다. 뒤늦게 ACK를 받으면 대기 항목을 제거합니다. 각 단계의 값과 전환 이유를 비교합니다.
UDP · 학습용 재전송
ACK와 재시도만으로 중복 실행이나 업무 완료가 보장되지는 않는다
본문 코드는 ACK와 제한된 재전송의 구조를 보여줍니다. 패킷 재수신·상대 검증·동시 접근·기한·혼잡 제어의 책임은 별도로 남아 있습니다.
| 코드 경로 | 현재 동작 | 보장하지 않는 것 |
|---|---|---|
| send_reliable | 보낸 패킷을 pending_acks에 저장 | 최대 재시도 뒤에도 전달되지 않을 수 있음 |
| receive: 데이터 | RELIABLE이면 ACK를 보내고 payload 반환 | 같은 seq 재수신도 다시 반환; 중복 실행 방지 없음 |
| receive: ACK | 같은 seq의 대기 항목 삭제 | 보낸 상대 주소를 대조하지 않음; 업무 처리 완료 증거 아님 |
| 타이머 / 수신 루프 | 호출자가 check_retransmits와 receive를 실행해야 함 | dict 동시 변경 보호·수신 기한·혼잡 제어 없음 |
- send_reliable
- 현재 동작: 보낸 패킷을 pending_acks에 저장보장하지 않는 것: 최대 재시도 뒤에도 전달되지 않을 수 있음
- receive: 데이터
- 현재 동작: RELIABLE이면 ACK를 보내고 payload 반환보장하지 않는 것: 같은 seq 재수신도 다시 반환; 중복 실행 방지 없음
- receive: ACK
- 현재 동작: 같은 seq의 대기 항목 삭제보장하지 않는 것: 보낸 상대 주소를 대조하지 않음; 업무 처리 완료 증거 아님
- 타이머 / 수신 루프
- 현재 동작: 호출자가 check_retransmits와 receive를 실행해야 함보장하지 않는 것: dict 동시 변경 보호·수신 기한·혼잡 제어 없음
코드 검토에 따른 범위입니다. 패킷 손실·재전송·중복 처리를 실행해 관측한 결과는 아닙니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 첫 전달부터 ACK 수신까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | 같은 seq 7 데이터가 도착하여 ACK를 보내지만 첫 ACK만 유실됩니다. 송신 쪽 대기 항목이 남아 타이머 검사 뒤 같은 데이터를 재전송합니다. 원 코드는 중복 seq를 억제하지 않아 payload를 두 번 반환합니다. ACK는 업무 처리 완료를 뜻하지 않습니다. |
| 표의 값 | send_reliable — 현재 동작: 보낸 패킷을 pending_acks에 저장; 보장하지 않는 것: 최대 재시도 뒤에도 전달되지 않을 수 있음. receive: 데이터 — 현재 동작: RELIABLE이면 ACK를 보내고 payload 반환; 보장하지 않는 것: 같은 seq 재수신도 다시 반환; 중복 실행 방지 없음. receive: ACK — 현재 동작: 같은 seq의 대기 항목 삭제; 보장하지 않는 것: 보낸 상대 주소를 대조하지 않음; 업무 처리 완료 증거 아님. 타이머 / 수신 루프 — 현재 동작: 호출자가 check_retransmits와 receive를 실행해야 함; 보장하지 않는 것: dict 동시 변경 보호·수신 기한·혼잡 제어 없음. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 전달 순번 7을 받아 ACK하고 payload를 반환합니다. |
|
|
| ACK 유실 첫 ACK가 유실돼 송신 대기 항목이 남습니다. |
|
|
| 재시도 검사 호출자가 재시도 간격 초과를 검사합니다. |
|
|
| 중복 수신 예제 코드는 순번 중복을 억제하지 않습니다. |
|
|
| ACK 수신 뒤늦게 ACK를 받으면 대기 항목을 제거합니다. |
|
|
이 코드는 ACK와 제한된 재전송의 구조만 보여주는 학습용 예제입니다. 전체 프로토콜 구현은 아닙니다.
위치 업데이트는 ACK 없이 보내고(어차피 곧 새 위치가 올 것이므로), 스킬 사용이나 아이템 획득 같은 이벤트는 ACK를 요구합니다.
이런 부분적 신뢰성을 제공하는 라이브러리(ENet, RakNet 등)도 존재합니다.
원 코드에서 ACK가 없어지는 경우: seq=7의 RELIABLE 데이터가 수신됐고 receive()가 ACK를 보냈지만 그 ACK만 손실됐다고 합시다. 송신 측에는 대기 항목이 남습니다. 호출자가 나중에 check_retransmits()를 실행해 0.5초 초과를 확인하면 같은 seq=7을 다시 보내며, 수신 측은 그 payload를 다시 반환합니다. 이미 받은 seq를 기억해 중복을 억제하는 처리가 없기 때문입니다. ACK는 receive() 안에서 payload 반환보다 먼저 전송하므로 업무 로직이 성공했다고 확인하는 응답도 아닙니다.
독립 추적: 수신 측 호출자가 반환된 payload마다 아이템을 하나 추가했습니다. 위 조건에서 최초 수신과 재전송을 모두 처리하면 몇 개가 추가될까요? 이어 ACK 없이 첫 전송 뒤 재시도 세 번까지 모두 소진한 원 코드에서 총 전송 횟수와 마지막 대기 항목의 처리를 설명하세요. 호출자는 타이머 검사를 계속 실행하며 다른 동시 변경은 없습니다.
재시도와 중복 실행을 따로 세기
같은 payload가 두 번 반환되므로 그 호출자라면 아이템도 두 번 추가합니다. 재시도는 원래 전송과 별개로 최대 세 번이어서 총 전송은 네 번입니다. 세 번째 재시도 뒤에도 ACK가 없고 다음 검사에서 간격을 초과하면 경고하고 대기 항목을 삭제하며 다섯 번째 전송은 하지 않습니다. 원문의 “손실 불가” 주석은 요구를 나타낼 뿐 이 코드가 최종 전달이나 정확히 한 번의 업무 실행을 보장한다는 뜻은 아닙니다. 요청 식별자와 업무 결과의 중복 처리 규칙을 별도로 설계해야 합니다.
원 조건식은 >이므로 시간 차가 정확히 0.5초일 때는 재시도하지 않습니다. 호출 주기와 스케줄링도 있어 “0.5초마다 정확히 자동 실행”하는 코드가 아닙니다. ACK의 상대 주소를 검증하지 않고 5바이트 미만 입력도 검사하지 않는 등 표의 책임이 남아 있으므로 이 조각을 완성된 운영용 전송 계층으로 사용하지 않습니다.
필요한 보장과 기한을 정한 뒤 검증된 전송 라이브러리의 동작과 대조해야 합니다.
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을 살펴보겠습니다.