icon

안동민 개발노트

7장 : UDP와 전송 계층 비교

TCP vs UDP 판단 기준


TCP는 신뢰성을 보장하고, UDP는 보장하지 않습니다. 그렇다면 TCP가 항상 더 좋은 선택일까요? 그렇지 않습니다. 신뢰성이라는 강력한 보장에는 반드시 비용이 따릅니다.

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)TCPHTML/CSS/JS 한 바이트라도 빠지면 렌더링 실패
웹 (HTTP/3)QUIC (UDP 기반)TCP HOL Blocking 해결 + 0-RTT 재연결
온라인 게임 (위치 동기화)UDP0.5초 전 위치보다 최신 위치가 중요
온라인 게임 (아이템 구매)TCP거래 데이터는 손실 불가
영상 스트리밍 (실시간)UDP (RTP)끊김 > 지연, 이전 프레임 재전송 무의미
영상 스트리밍 (VOD)TCP (HLS/DASH)버퍼링 가능, 완전한 파일 전달 필요
DNS 조회UDP작은 패킷, 연결 오버헤드 > 데이터
DNS 영역 전송TCP대량 데이터, 완전성 필수
VoIP (음성 통화)UDP (RTP)실시간성이 핵심, 과거 음성 재전송 무의미
파일 전송 (FTP, SFTP)TCP1바이트 누락 = 파일 손상
IoT 센서 데이터UDP (CoAP)저전력, 소량 데이터, 최신 값만 의미
이메일 (SMTP)TCP메일 내용 완전 전달 필수
NTP (시간 동기화)UDP작은 패킷, 연결 불필요
DHCPUDP브로드캐스트 필요, IP 할당 전이므로 TCP 불가
하나의 게임에서 TCP + UDP 동시 사용
게임 서버

UDP (실시간)
┌────────────┐
│ 위치 동기화│
│ 총알 궤적  │
│ 이펙트     │
│ 음성 채팅  │
└────────────┘
특성: 손실 허용, 낮은 지연 필요

TCP (중요 데이터)
┌────────────┐
│ 로그인     │
│ 아이템 거래│
│ 채팅       │
│ 매치 결과  │
└────────────┘
특성: 손실 불가, 순서 + 완전성 필수

지연 민감도 vs 신뢰성 트레이드오프

패턴이 보이시나요? 핵심은 지연 민감도(Latency Sensitivity)신뢰성 요구 수준의 트레이드오프입니다.

프로토콜 선택 매트릭스
                 신뢰성 요구
          낮음 ◄─────────────► 높음
         ┌─────────────┬─────────────┐
    높음 │  UDP        │  QUIC       │
         │ (게임,VoIP) │  (HTTP/3)   │
  지연   │             │             │
  민감도 ├─────────────┼─────────────┤
         │  UDP        │  TCP        │
    낮음 │  (DNS,IoT)  │ (HTTP,FTP)  │
         │             │  (이메일)   │
         └─────────────┴─────────────┘

TCP의 재전송은 신뢰성을 보장하지만, 재전송으로 인한 지연이 발생합니다. 더 큰 문제는 Head-of-Line(HOL) Blocking입니다. TCP는 순서를 보장하기 때문에, 중간에 하나의 패킷이 손실되면 그 뒤의 데이터가 모두 수신 버퍼에서 대기해야 합니다.

TCP HOL Blocking 문제
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% 신뢰성은 필요 없지만, 완전히 무시할 수도 없는 상황입니다.

이때는 애플리케이션 레벨에서 필요한 만큼만 신뢰성을 구현합니다.

reliable_udp.py
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을 살펴보겠습니다.

목차