본문 바로가기

TCP vs UDP 판단 기준

TCP가 주는 보장과 그 대가를 UDP와 나란히 놓고, 지연과 신뢰성 가운데 무엇이 서비스에 중요한지로 프로토콜을 고르는 기준을 세웁니다. 사용 사례별 선택을 정리하고, UDP 위에 순번과 ACK, 재전송, 중복 제거를 필요한 만큼만 더하는 코드를 손실 상황에서 따라갑니다.

TCP는 빠진 데이터를 다시 보내고 순서를 맞춰 주지만 UDP는 그러지 않습니다. 그렇다면 언제나 TCP를 쓰는 편이 나을까요? 그렇지 않습니다. TCP의 보장에는 연결을 맺는 왕복, 재전송을 기다리는 지연, 한 바이트가 빠지면 뒤의 데이터까지 멈추는 성질 같은 대가가 따릅니다. 이 글에서는 두 프로토콜이 무엇을 주고 무엇을 받아 가는지 비교하고, 서비스의 요구에 맞춰 고르는 기준과 UDP 위에 필요한 기능만 더하는 방법을 살펴봅니다.

무엇을 보장받고 무엇을 지불하나

항목TCPUDP
첫 데이터를 보내기까지핸드셰이크로 1 RTT, TLS를 쓰면 그 이상바로 보낸다
손실다시 보내 메운다. 그동안 기다린다메우지 않는다. 필요하면 애플리케이션이 한다
순서맞춰서 넘긴다. 빈 곳이 채워질 때까지 뒤의 데이터도 기다린다도착한 순서대로 넘긴다
메시지 경계없다. 애플리케이션이 나눈다데이터그램 하나가 메시지 하나
혼잡 제어운영체제가 한다없다. 많이 보낸다면 애플리케이션이 해야 한다
서버가 기억하는 상태연결마다 버퍼와 상태소켓 하나로 모든 클라이언트를 받을 수 있다
중간 장비거의 모든 방화벽과 프록시가 통과시킨다막는 망이 있고, NAT의 연결 정보도 빨리 지워진다

마지막 행은 실무에서 자주 잊습니다. 회사나 공공 와이파이의 방화벽 가운데는 DNS 말고는 UDP를 막는 곳이 있고, NAT 장비는 조용한 UDP 흐름의 정보를 TCP보다 훨씬 빨리 지웁니다. 그래서 UDP를 쓰는 서비스는 UDP가 막혔을 때 TCP나 HTTPS로 넘어가는 길을 함께 마련해 두는 경우가 많습니다.

사용 사례별 선택

사용 사례고르는 프로토콜이유
웹 (HTTP/1.1, HTTP/2)TCP문서와 파일을 빠짐없이 순서대로 받아야 한다
웹 (HTTP/3)QUIC (UDP 위)스트림마다 손실을 따로 복구해 한 스트림의 손실이 다른 스트림을 막지 않는다
게임의 위치 동기화UDP늦게 온 0.5초 전 위치보다 방금 온 위치가 중요하다
게임의 아이템 구매TCP 또는 확인과 재전송을 갖춘 UDP결과가 빠짐없이 정확히 한 번 반영되어야 한다
실시간 음성과 영상 통화UDP (RTP)재생 시점을 넘긴 데이터는 쓸모가 없다
동영상 다시 보기HTTP (TCP 또는 QUIC)미리 받아 버퍼에 쌓을 수 있으므로 완전성이 더 중요하다
DNS 조회주로 UDP, 필요하면 TCP질문 하나에 답 하나로 끝나는 작은 교환이다
DNS 영역 전송TCP큰 데이터를 빠짐없이 옮겨야 한다
파일 전송, 이메일TCP1바이트만 빠져도 파일이나 메일이 망가진다
시간 동기화 (NTP)UDP작은 패킷 하나로 끝나고, 재전송의 지연이 오히려 시간을 틀리게 한다
DHCPUDP주소가 없는 상태에서 브로드캐스트로 서버를 찾아야 한다
IoT 센서 (CoAP)UDP작은 메시지가 대부분이고, 필요한 메시지만 확인을 받는다

DNS는 "조회는 UDP, 영역 전송은 TCP"로 외우기 쉽지만, 조회도 TCP를 지원해야 합니다. 응답이 UDP로 보내기에 너무 크면 서버는 잘렸다는 표시를 붙여 보내고, 클라이언트는 같은 질문을 TCP로 다시 합니다. DNSSEC 서명처럼 응답이 커지는 기능이 늘면서 이 경우가 많아졌습니다.

지연과 신뢰성의 저울

TCP의 재전송은 빠진 데이터를 메워 주지만 그만큼 기다리게 합니다. 더 큰 문제는 HOL 블로킹(head-of-line blocking)입니다. TCP는 바이트 스트림 하나를 순서대로 넘기므로, 중간의 세그먼트 하나가 빠지면 그 뒤에 이미 도착한 데이터도 빈 곳이 채워질 때까지 애플리케이션에 넘기지 못합니다. 영상 통화라면 프레임 하나가 빠졌다고 뒤의 프레임까지 모두 멈추는 셈이어서, 빠진 프레임은 건너뛰고 다음 프레임을 보여 주는 편이 사용자에게 훨씬 낫습니다.

판단의 핵심은 데이터마다 쓸모 있는 기한(deadline)이 있느냐입니다. 음성 통화는 한쪽 방향의 지연이 150ms를 넘지 않는 것이 좋다고 알려져 있는데, RTT가 120ms인 경로에서 빠진 패킷을 다시 받으려면 받는 쪽이 손실을 알아채고, 다시 보내 달라고 알리고, 재전송이 도착하기까지 적어도 한 번의 왕복이 더 듭니다. 이미 기한을 넘긴 데이터를 다시 받아 봐야 재생할 수 없으므로, 이런 서비스에는 재전송이 없는 UDP가 맞습니다. 반대로 파일은 아무리 늦어도 빠진 바이트가 있으면 안 되므로 기다려서라도 메워야 합니다.

  • 데이터가 하나도 빠지면 안 되고 기다릴 수 있다면 TCP를 고릅니다.
  • 늦게 온 데이터가 쓸모없다면 UDP를 고르고, 손실은 다음 데이터로 덮거나 건너뜁니다.
  • 작은 질문과 답이고 다시 묻기가 쉽다면 UDP로 연결의 왕복을 아낄 수 있습니다.
  • 순서와 완전성은 필요하지만 여러 흐름이 서로를 막지 않아야 한다면 QUIC을 검토합니다.
  • UDP를 골랐다면 UDP가 막힌 망에서 TCP로 넘어가는 길을 함께 설계합니다.

UDP 위에 필요한 만큼 더하기

UDP를 고른 서비스도 일부 메시지는 반드시 전달되어야 하는 경우가 많습니다. 게임이라면 위치 정보는 곧 새 값이 오므로 잃어도 되지만, 아이템을 얻었다는 사건은 잃으면 안 됩니다. 이럴 때는 메시지마다 순번을 붙이고, 중요한 메시지에만 ACK를 받아 일정 시간 안에 ACK가 없으면 다시 보내는 기능을 애플리케이션이 직접 더합니다.

다음 코드는 그 구조를 보여 줍니다. 실제 소켓 대신 패킷을 상대에게 넘기는 함수를 바깥에서 받고, 시각도 인자로 넘겨서 손실 상황을 정해진 대로 재현합니다. check_retransmits()는 마지막 전송 뒤 0.5초가 지난 메시지를 다시 보내고, 세 번 다시 보내도 ACK가 없으면 포기합니다. 받는 쪽은 데이터를 받으면 ACK부터 보내고, dedupe가 켜져 있으면 이미 넘긴 순번을 다시 넘기지 않습니다.

import struct

RELIABLE, ACK = 1, 2

class ReliableUDP:
    def __init__(self, transport, dedupe=False, retry_interval=0.5, max_retries=3):
        self.transport = transport                       # 상대에게 패킷을 넘기는 함수
        self.dedupe = dedupe
        self.retry_interval = retry_interval
        self.max_retries = max_retries
        self.pending_acks = {}                           # seq -> (패킷, 마지막으로 보낸 시각, 재시도 횟수)
        self.seen = set()
        self.seq = 0

    def send_reliable(self, payload, now):
        self.seq += 1
        packet = struct.pack("!BI", RELIABLE, self.seq) + payload
        self.pending_acks[self.seq] = (packet, now, 0)
        self.transport(packet)

    def check_retransmits(self, now):
        for seq, (packet, sent_at, retries) in list(self.pending_acks.items()):
            if now - sent_at > self.retry_interval:
                if retries >= self.max_retries:
                    print(f"seq={seq} 포기: 재시도 {retries}번")
                    del self.pending_acks[seq]
                else:
                    self.pending_acks[seq] = (packet, now, retries + 1)
                    self.transport(packet)

    def receive(self, packet):
        kind, seq = struct.unpack("!BI", packet[:5])
        if kind == ACK:
            self.pending_acks.pop(seq, None)             # 확인된 순번은 대기 목록에서 지운다
            return None
        self.transport(struct.pack("!BI", ACK, seq))     # 데이터를 받으면 ACK부터 보낸다
        if self.dedupe:
            if seq in self.seen:
                return None                              # 이미 넘긴 순번은 다시 넘기지 않는다
            self.seen.add(seq)
        return packet[5:]

def run(dedupe, lost_acks, check_times):
    inventory, sends = [], 0

    def to_receiver(packet):                             # 보내는 쪽 → 받는 쪽
        nonlocal sends
        sends += 1
        payload = receiver.receive(packet)
        if payload is not None:
            inventory.append(payload.decode())           # 받은 사건마다 아이템을 하나 넣는다

    def to_sender(packet):                               # 받는 쪽 → 보내는 쪽(ACK)
        nonlocal lost_acks
        if lost_acks > 0:
            lost_acks -= 1                               # 이 ACK는 사라진다
        else:
            sender.receive(packet)

    sender = ReliableUDP(to_receiver)
    receiver = ReliableUDP(to_sender, dedupe=dedupe)
    sender.send_reliable(b"sword", now=0.0)
    for now in check_times:
        sender.check_retransmits(now)
    label = "중복 제거 있음" if dedupe else "중복 제거 없음"
    print(f"{label}: 전송 {sends}번, 인벤토리 {inventory}, 남은 대기 {list(sender.pending_acks)}")

run(False, lost_acks=1, check_times=[0.6])
run(True, lost_acks=1, check_times=[0.6])
run(True, lost_acks=99, check_times=[0.6, 1.2, 1.8, 2.4])
중복 제거 없음: 전송 2번, 인벤토리 ['sword', 'sword'], 남은 대기 []
중복 제거 있음: 전송 2번, 인벤토리 ['sword'], 남은 대기 []
seq=1 포기: 재시도 3번
중복 제거 있음: 전송 4번, 인벤토리 ['sword'], 남은 대기 []

첫 실행에서는 데이터는 잘 도착했는데 ACK만 사라졌습니다. 보내는 쪽은 상대가 받았는지 알 수 없으므로 0.6초에 같은 순번을 다시 보내고, 중복을 걸러 내지 않는 받는 쪽은 같은 사건을 두 번 넘겨 아이템이 두 개가 되었습니다. 두 번째 실행은 받는 쪽이 이미 본 순번을 기억해 두 번째 사건을 버렸습니다.

재전송 예제가 처리하는 것과 남기는 것

순번 7을 받아 ACK하고 payload를 반환합니다. 뒤늦게 ACK를 받으면 대기 항목을 제거합니다. 각 단계의 값과 전환 이유를 비교합니다.

UDP · 학습용 재전송

ACK와 재시도만으로 중복 실행이나 업무 완료가 보장되지는 않는다

본문 코드는 ACK와 제한된 재전송의 구조를 보여줍니다. 패킷 재수신·상대 검증·동시 접근·기한·혼잡 제어의 책임은 별도로 남아 있습니다.

ACK 유실 뒤 같은 payload가 다시 반환되는 원 코드같은seq7데이터가도착하여ACK를보내지만첫ACK만유실된다. 송신쪽대기항목이남아타이머검사뒤같은데이터를재전송한다. 원코드는중복seq를억제하지않아payload를두번반환한다. ACK는업무처리완료를뜻하지않는다.RELIABLEseq=7ACK 유실같은 데이터seq=7ACK 도착송신자수신자payload반환payload반환재시도 검사0.5초 초과
ACK 유실 뒤 같은 payload가 다시 반환되는 원 코드같은seq7데이터가도착하여ACK를보내지만첫ACK만유실된다. 송신쪽대기항목이남아타이머검사뒤같은데이터를재전송한다. 원코드는중복seq를억제하지않아payload를두번반환한다. ACK는업무처리완료를뜻하지않는다.RELIABLEseq=7ACK 유실같은 데이터seq=7ACK 도착송신자수신자payload반환payload반환재시도 검사0.5초 초과
재전송 예제가 처리하는 것과 남기는 것
코드 경로현재 동작보장하지 않는 것
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를 반환합니다.

  • 순번·수신값 1: seq7 pending
  • 순번·수신값 2: payload 반환 1
  • ACK 송신은 payload 반환보다 먼저; 업무 완료 확인은 아님
ACK 유실

첫 ACK가 유실돼 송신 대기 항목이 남습니다.

  • 순번·수신값 1: pending 7
  • 순번·수신값 2: 첫 ACK 손실
  • 순번 7을 pending으로 유지
재시도 검사

호출자가 재시도 간격 초과를 검사합니다.

  • 순번·수신값 1: elapsed>0.5s
  • 순번·수신값 2: seq 7 동일
  • elapsed > 0.5초일 때 같은 순번 재전송; 정시 자동 실행은 아님
중복 수신

예제 코드는 순번 중복을 억제하지 않습니다.

  • 순번·수신값 1: payload 반환 2
  • 순번·수신값 2: ACK 재전송
  • ACK를 다시 송신한 뒤 payload를 두 번째로 반환; 호출자가 항목을 두 번 추가할 수 있음
ACK 수신

뒤늦게 ACK를 받으면 대기 항목을 제거합니다.

  • 순번·수신값 1: pending 7 없음
  • 상대 주소 검증은 없음; 재시도 3번 소진 시 총 4회 전송으로 종료

재전송 예제가 처리하는 것과 남기는 것

순번 7을 받아 ACK하고 payload를 반환합니다. 뒤늦게 ACK를 받으면 대기 항목을 제거합니다. 각 단계의 값과 전환 이유를 비교합니다.

UDP · 학습용 재전송

ACK와 재시도만으로 중복 실행이나 업무 완료가 보장되지는 않는다

본문 코드는 ACK와 제한된 재전송의 구조를 보여줍니다. 패킷 재수신·상대 검증·동시 접근·기한·혼잡 제어의 책임은 별도로 남아 있습니다.

ACK 유실 뒤 같은 payload가 다시 반환되는 원 코드같은seq7데이터가도착하여ACK를보내지만첫ACK만유실된다. 송신쪽대기항목이남아타이머검사뒤같은데이터를재전송한다. 원코드는중복seq를억제하지않아payload를두번반환한다. ACK는업무처리완료를뜻하지않는다.RELIABLEseq=7ACK 유실같은 데이터seq=7ACK 도착송신자수신자payload반환payload반환재시도 검사0.5초 초과
ACK 유실 뒤 같은 payload가 다시 반환되는 원 코드같은seq7데이터가도착하여ACK를보내지만첫ACK만유실된다. 송신쪽대기항목이남아타이머검사뒤같은데이터를재전송한다. 원코드는중복seq를억제하지않아payload를두번반환한다. ACK는업무처리완료를뜻하지않는다.RELIABLEseq=7ACK 유실같은 데이터seq=7ACK 도착송신자수신자payload반환payload반환재시도 검사0.5초 초과
재전송 예제가 처리하는 것과 남기는 것
코드 경로현재 동작보장하지 않는 것
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를 반환합니다.

  • 순번·수신값 1: seq7 pending
  • 순번·수신값 2: payload 반환 1
  • ACK 송신은 payload 반환보다 먼저; 업무 완료 확인은 아님
ACK 유실

첫 ACK가 유실돼 송신 대기 항목이 남습니다.

  • 순번·수신값 1: pending 7
  • 순번·수신값 2: 첫 ACK 손실
  • 순번 7을 pending으로 유지
재시도 검사

호출자가 재시도 간격 초과를 검사합니다.

  • 순번·수신값 1: elapsed>0.5s
  • 순번·수신값 2: seq 7 동일
  • elapsed > 0.5초일 때 같은 순번 재전송; 정시 자동 실행은 아님
중복 수신

예제 코드는 순번 중복을 억제하지 않습니다.

  • 순번·수신값 1: payload 반환 2
  • 순번·수신값 2: ACK 재전송
  • ACK를 다시 송신한 뒤 payload를 두 번째로 반환; 호출자가 항목을 두 번 추가할 수 있음
ACK 수신

뒤늦게 ACK를 받으면 대기 항목을 제거합니다.

  • 순번·수신값 1: pending 7 없음
  • 상대 주소 검증은 없음; 재시도 3번 소진 시 총 4회 전송으로 종료

세 번째 실행은 ACK가 모두 사라지는 경우입니다. 처음 전송과 재시도 세 번으로 모두 네 번을 보낸 뒤, 2.4초의 검사에서 포기합니다. 그런데 인벤토리를 보면 받는 쪽은 처음부터 아이템을 받았습니다. 보내는 쪽이 포기했다는 것은 "전달되지 않았다"가 아니라 "전달되었는지 모른다"는 뜻입니다. 이런 결과를 다루려면 사건마다 고유한 요청 ID를 붙여 받는 쪽이 같은 요청을 한 번만 처리하게 하고, 결과가 불확실할 때 보내는 쪽이 처리 여부를 다시 물어볼 수 있게 해야 합니다. ACK도 받는 쪽 프로그램이 패킷을 받았다는 뜻일 뿐, 아이템 지급 같은 업무가 끝났다는 뜻은 아닙니다.

이 코드는 구조를 보여 주는 모형이어서 실제로 쓰려면 더 필요한 것이 많습니다. 보낸 쪽 주소를 확인하지 않으면 누구나 가짜 ACK를 보낼 수 있고, seen은 영원히 커지므로 최근 순번의 범위만 기억하는 방식으로 바꿔야 합니다. check_retransmits()를 주기적으로 부르는 타이머, 경로에 맞춘 재전송 간격, 많이 보낼 때의 혼잡 제어도 필요합니다. 게임 업계에서는 이런 기능을 갖춘 ENet 같은 라이브러리를 쓰거나, 입력을 보낼 때 최근 몇 개의 입력을 함께 실어 하나쯤 사라져도 다음 패킷으로 메우는 방식을 쓰기도 합니다. 범용으로 이 모든 것을 갖춘 것이 다음 글의 QUIC입니다.

더 깊이: UDP로 얼마나 자주 보내도 되나

UDP에는 혼잡 제어가 없으므로, 보내는 속도의 한도는 애플리케이션이 스스로 지켜야 합니다. RFC 8085(BCP 145)는 이 한도를 데이터의 양에 따라 나누어 권합니다. 많은 데이터를 보내는 애플리케이션은 TCP와 비슷한 윈도우 기반 혼잡 제어나 TFRC 같은 방식을 갖추어야 하고, 혼잡 제어가 없다면 경로가 심하게 막혔을 때 전송을 멈추는 회로 차단기(circuit breaker)라도 두어야 합니다.

본문의 게임 메시지처럼 데이터가 적은 애플리케이션에도 규칙이 있습니다. 한 목적지에 평균적으로 RTT마다 데이터그램 하나보다 많이 보내지 않아야 하고, RTT를 모르지만 손실은 알 수 있다면 처음 1초로 정한 간격을 손실이 날 때마다 지수적으로 늘려 가며 보낼 수 있습니다. 돌아오는 트래픽이 아예 없어 RTT를 잴 수 없다면 3초에 데이터그램 하나를 넘지 않게 합니다. 본문 코드가 0.5초라는 고정 간격으로 다시 보내는 것은 구조를 보여 주기 위한 단순화이며, 실제로 쓸 때는 잰 RTT에 맞춘 간격과 재시도마다 늘어나는 대기를 넣어야 합니다.

정리

  • TCP는 손실 복구와 순서, 혼잡 제어를 주는 대신 연결의 왕복과 재전송의 대기, HOL 블로킹이라는 대가를 받습니다.
  • UDP는 기다림 없이 보내고 메시지 경계를 지키지만, 손실과 순서, 혼잡 제어는 애플리케이션의 몫이고 UDP를 막는 망에 대비해야 합니다.
  • 데이터에 쓸모 있는 기한이 있어 재전송이 그 기한 안에 올 수 없다면 UDP가, 늦더라도 빠짐없이 받아야 한다면 TCP가 맞습니다.
  • UDP 위에 순번과 ACK, 재전송을 더하면 ACK가 사라졌을 때 중복이 생기므로 받는 쪽의 중복 제거가 필요하고, 재전송을 포기한 결과는 "모른다"로 다루어야 합니다.
  • 주소 확인, 순번 범위 관리, 타이머, 혼잡 제어까지 직접 만들기보다 검증된 라이브러리나 QUIC을 먼저 검토합니다.

연습 문제

1. 서비스별 선택

다음 기능에 TCP와 UDP 가운데 무엇이 맞는지, 이유와 함께 고르세요. (가) 메신저의 대화 기록 동기화, (나) 화상 회의의 음성, (다) 초당 30번 보내는 게임 캐릭터의 위치, (라) 기기의 펌웨어 업데이트 내려받기.

해설 보기

(가)는 메시지가 하나라도 빠지거나 순서가 바뀌면 안 되고 몇백 밀리초 늦어도 괜찮으므로 TCP가 맞습니다. (나)는 재생 시점을 넘긴 음성은 쓸모가 없으므로 UDP 위의 RTP가 맞고, 손실은 앞뒤 소리로 메우는 은닉 기법으로 다룹니다. (다)는 33ms마다 새 위치가 오므로 사라진 위치를 다시 받을 필요가 없어 UDP가 맞습니다. (라)는 1바이트만 틀려도 기기가 망가질 수 있으므로 TCP로 받고, 받은 뒤 서명이나 해시로 한 번 더 확인합니다.

2. 중복 제거 없이 ACK가 모두 사라지면

본문 코드에서 run(False, lost_acks=99, check_times=[0.6, 1.2, 1.8, 2.4])를 실행하면 몇 번 전송되고, 인벤토리에는 아이템이 몇 개 들어가나요? 마지막에 보내는 쪽은 이 메시지에 대해 무엇을 알고 있나요?

해설 보기

처음 전송과 0.6초, 1.2초, 1.8초의 재시도로 모두 네 번 전송되고, 2.4초의 검사에서 재시도 횟수가 3이므로 seq=1 포기: 재시도 3번을 찍고 대기 목록에서 지웁니다. 다섯 번째 전송은 없습니다. 받는 쪽은 중복을 걸러 내지 않으므로 네 번 모두 아이템을 넣어 인벤토리에 네 개가 들어갑니다. 보내는 쪽은 ACK를 한 번도 받지 못했으므로 상대가 받았는지 모른 채 포기했을 뿐인데, 실제로는 같은 사건이 네 번 처리되었습니다. 재전송에는 받는 쪽의 중복 제거가 짝으로 따라가야 하는 이유입니다.

3. 재전송이 기한 안에 올 수 있을까

20ms마다 음성 패킷을 보내는 통화에서 한쪽 방향의 지연이 60ms이고, 패킷은 보낸 뒤 150ms 안에 도착해야 재생에 쓸 수 있다고 합시다. 받는 쪽은 다음 패킷이 도착했을 때 앞의 패킷이 빠졌다는 것을 알아채고 곧바로 재전송을 요청하며, 보내는 쪽도 요청을 받자마자 다시 보낸다면, 재전송된 패킷은 원래 보낸 시각으로부터 몇 ms 뒤에 도착하나요?

해설 보기

원래 패킷이 도착했어야 할 시각은 보낸 뒤 60ms이고, 다음 패킷은 20ms 뒤에 보내졌으므로 80ms에 도착해 손실을 알립니다. 재전송 요청이 보내는 쪽에 닿기까지 60ms가 걸려 140ms, 다시 보낸 패킷이 도착하기까지 60ms가 더 걸려 200ms가 됩니다. 150ms의 기한을 50ms나 넘기므로 재전송은 소용이 없고, 이 패킷은 앞뒤 소리로 메우는 편이 낫습니다. 같은 계산을 RTT가 20ms인 망에서 해 보면 재전송이 기한 안에 들어오므로, 같은 서비스라도 경로에 따라 판단이 달라질 수 있습니다.

다음 글 QUIC과 전송 계층의 진화에서는 UDP 위에서 여러 스트림과 암호화, 손실 복구, 연결 이동을 함께 제공하는 QUIC이 TCP 기반 웹의 지연을 어떻게 줄이는지 살펴봅니다.