Ray Book
네트워크 기초

TCP와 UDP

신뢰성 vs 속도, 3-way handshake, 흐름 제어, 혼잡 제어와 UDP의 단순함을 시각화합니다

csnetworktcpudptransport-layer

Transport 계층의 두 주역

이전 글에서 TCP/IP 4계층을 살펴봤습니다. 그 중 Transport 계층에는 두 가지 대표 프로토콜이 있습니다. 신뢰성을 보장하는 TCP속도에 집중하는 UDP입니다.

둘 다 Application 계층의 데이터를 Internet 계층으로 전달하는 같은 역할을 맡지만, 접근 방식이 완전히 다릅니다. TCP는 "하나도 놓치지 않겠다"이고, UDP는 "빠르게 보내고 끝"입니다.

TCP: 신뢰성 있는 전송

TCP (Transmission Control Protocol) 는 연결 지향 프로토콜입니다. 데이터를 보내기 전에 반드시 연결을 수립하고, 전송 중에는 순서와 무결성을 보장하며, 끝나면 정리합니다.

3-way Handshake

TCP 연결은 세 단계로 시작됩니다.

  1. SYN : 클라이언트가 서버에 "연결하고 싶다"는 신호를 보냅니다. 초기 시퀀스 번호를 포함합니다.
  2. SYN-ACK : 서버가 "알겠다, 나도 준비됐다"고 응답합니다. 서버의 초기 시퀀스 번호와 클라이언트 시퀀스에 대한 확인을 포함합니다.
  3. ACK : 클라이언트가 "확인했다"고 마지막 확인을 보냅니다. 이제 양방향 통신이 가능합니다.
// 3-way handshake를 개념적으로 표현
const handshake = [
  { from: "client", flag: "SYN", seq: 100 },
  { from: "server", flag: "SYN-ACK", seq: 300, ack: 101 },
  { from: "client", flag: "ACK", seq: 101, ack: 301 },
  // 이제 데이터 전송 가능
];

이 과정에서 양쪽은 시퀀스 번호를 교환합니다. 이 번호가 이후 데이터의 순서 보장유실 감지의 기준이 됩니다.

순서 보장과 재전송

TCP는 각 세그먼트에 시퀀스 번호 (Sequence Number) 를 부여합니다. 수신 측은 이 번호를 보고 원래 순서대로 데이터를 재조립합니다. 패킷이 유실되면 중복 ACK 또는 타임아웃을 통해 감지하고, 송신 측이 해당 세그먼트를 재전송합니다.

// TCP 재전송 시나리오
const scenario = {
  sent: [
    { seq: 1, data: "Hello" },     // 도착
    { seq: 2, data: "World" },     // 유실!
    { seq: 3, data: "!" },         // 도착 (but 순서 밀림)
  ],
  received: [
    { seq: 1, data: "Hello" },
    // seq 2가 없다 -> 중복 ACK 전송
    { seq: 3, data: "!" },         // 버퍼에 보관
  ],
  retransmit: { seq: 2, data: "World" }, // 재전송
  // 최종 결과: "Hello World !" (순서 보장)
};

흐름 제어: 슬라이딩 윈도우

수신 측이 데이터를 처리하는 속도보다 송신 측이 보내는 속도가 빠르면 문제가 생깁니다. TCP는 슬라이딩 윈도우 (Sliding Window) 로 이를 해결합니다.

수신 측은 자신의 수신 윈도우 (rwnd) 크기를 ACK에 포함해 알려줍니다. 송신 측은 ACK를 받지 않은 데이터의 총량이 이 윈도우를 넘지 않도록 제한합니다.

// 슬라이딩 윈도우 개념
const flowControl = {
  receiverWindow: 4,  // 수신 측: "4개까지 받을 수 있어"
  sender: {
    sent: [1, 2, 3, 4],   // 윈도우만큼 전송
    waitForAck: true,       // ACK를 기다려야 더 보낼 수 있음
  },
  // ACK 3 + window 6 도착 -> 1,2 확인, 윈도우 확장
  afterAck: {
    confirmed: [1, 2],
    canSend: [3, 4, 5, 6, 7, 8], // 새 윈도우 범위
  },
};

윈도우 크기는 동적으로 변합니다. 수신 측 버퍼에 여유가 있으면 커지고, 부족하면 줄어듭니다. 극단적으로 윈도우가 0이 되면 송신 측은 전송을 중단합니다.

혼잡 제어

흐름 제어가 수신 측의 처리 능력을 고려한다면, 혼잡 제어는 네트워크 자체의 상태를 고려합니다.

TCP는 혼잡 윈도우 (cwnd) 라는 별도의 변수를 관리합니다. 실제 전송량은 수신 윈도우와 혼잡 윈도우 중 작은 값으로 결정됩니다.

주요 알고리즘:

  • Slow Start : 연결 초기에 cwnd를 작은 값에서 시작해 ACK를 받을 때마다 지수적으로 증가시킵니다. 초기 윈도우는 RFC 6928 기준 10 세그먼트가 현대 OS의 기본값입니다.
  • Congestion Avoidance : cwnd가 임계값 (ssthresh) 에 도달하면 선형적으로 증가합니다.
  • Fast Retransmit : 3개의 중복 ACK를 받으면 타임아웃을 기다리지 않고 즉시 재전송합니다.
  • Fast Recovery : 유실 후 cwnd를 절반으로 줄이고, Slow Start 대신 Congestion Avoidance 단계로 진입합니다.

이 방식을 AIMD (Additive Increase, Multiplicative Decrease) 라고 합니다. 조금씩 올리고, 문제가 생기면 확 줄이는 전략입니다.

UDP: 단순하고 빠른 전송

UDP (User Datagram Protocol) 는 비연결 프로토콜입니다. 연결 수립, 순서 보장, 재전송, 흐름 제어, 혼잡 제어 모두 없습니다.

UDP 헤더의 단순함

// UDP 헤더 구조 (8바이트)
const udpHeader = {
  srcPort: 12345,       // 출발지 포트 (2바이트)
  destPort: 53,         // 목적지 포트 (2바이트) - DNS
  length: 42,           // 헤더 + 데이터 길이 (2바이트)
  checksum: 0xABCD,     // 체크섬 (2바이트, 선택적)
};

// TCP 헤더 구조 (20~60바이트)
const tcpHeader = {
  srcPort: 52431,       // 출발지 포트 (2바이트)
  destPort: 443,        // 목적지 포트 (2바이트)
  sequenceNum: 1,       // 시퀀스 번호 (4바이트)
  ackNum: 0,            // 확인 번호 (4바이트)
  dataOffset: 5,        // 헤더 길이 (4비트)
  flags: 0b00000010,    // 제어 비트 (8비트, CWR·ECE·URG·ACK·PSH·RST·SYN·FIN)
  windowSize: 65535,    // 수신 윈도우 (2바이트)
  checksum: 0x1234,     // 체크섬 (2바이트)
  urgentPtr: 0,         // 긴급 포인터 (2바이트)
  // + 옵션 (0~40바이트)
};

TCP 헤더가 최소 20바이트인 데 비해, UDP 헤더는 딱 8바이트입니다. 이 차이가 오버헤드와 처리 속도에 직접 영향을 줍니다.

UDP를 선택하는 이유

UDP에는 보장이 없는 대신, 분명한 장점이 있습니다.

  • 낮은 지연 : 핸드셰이크가 없어 첫 데이터 전송까지의 시간이 짧습니다.
  • 적은 오버헤드 : 헤더가 작고, ACK나 상태 관리가 불필요합니다.
  • 멀티캐스트/브로드캐스트 : TCP는 1:1 연결만 지원하지만, UDP는 하나의 패킷을 여러 수신자에게 동시에 보낼 수 있습니다.
  • 애플리케이션 제어 : 전송 방식을 애플리케이션이 직접 결정할 수 있어 유연합니다.

TCP vs UDP 시각화

아래 시각화에서 TCP와 UDP가 각 단계에서 어떻게 다르게 동작하는지 비교하세요.

01연결 수립 (Connection Setup)
TCP3-way Handshake
Client-->SYN
Server<--SYN-ACK
Client-->ACK

TCP는 데이터 전송 전 반드시 연결을 수립합니다. 클라이언트가 SYN을 보내고, 서버가 SYN-ACK로 응답하면, 클라이언트가 ACK로 확인합니다. 3번의 패킷 교환이 필요합니다.

UDP연결 없음
(패킷 교환 없음)

UDP는 연결 수립 과정이 없습니다. 별도의 핸드셰이크 없이 바로 데이터를 전송할 수 있어 초기 지연이 0입니다.

TCP는 안정적인 연결을 먼저 확보하고, UDP는 즉시 전송을 시작합니다.

TCP vs UDP 비교표

특성TCPUDP
연결 방식연결 지향 (3-way handshake)비연결
순서 보장O (시퀀스 번호)X
재전송O (타임아웃, 중복 ACK)X
흐름 제어O (슬라이딩 윈도우)X
혼잡 제어O (Slow Start, AIMD)X
헤더 크기20~60 바이트8 바이트
전송 단위세그먼트 (Segment)데이터그램 (Datagram)
통신 방식1:11:1, 1:N, N:N

실무에서의 선택

TCP를 사용하는 경우:

  • 웹 통신 (HTTP/1.1, HTTP/2): 모든 데이터가 정확히 도착해야 합니다. HTML이 깨지면 페이지를 렌더링할 수 없습니다.
  • 파일 전송 (FTP, SFTP): 1비트라도 유실되면 파일이 손상됩니다.
  • 이메일 (SMTP, IMAP): 메일 내용이 빠지면 안 됩니다.
  • 데이터베이스 연결 : 쿼리 결과가 정확해야 합니다.

UDP를 사용하는 경우:

  • 실시간 게임 : 0.1초 전 캐릭터 위치보다 지금 위치가 중요합니다. 유실된 패킷을 재전송받을 때쯤이면 이미 쓸모없는 데이터입니다.
  • 라이브 스트리밍 : 영상의 한 프레임이 빠져도 다음 프레임으로 넘어가면 됩니다. 끊김 없는 재생이 완벽한 화질보다 중요합니다.
  • DNS 질의 : 짧은 요청-응답이므로 연결 수립 오버헤드가 아깝습니다.
  • VoIP (인터넷 전화): 실시간성이 핵심입니다.

QUIC: UDP 위의 신뢰성

HTTP/3가 사용하는 QUIC 프로토콜은 흥미로운 선택을 합니다. Transport 계층에서 UDP를 사용하되, 애플리케이션 레벨에서 TCP와 유사한 신뢰성 (순서 보장, 재전송, 혼잡 제어) 을 직접 구현합니다.

// QUIC의 접근 방식을 개념적으로 표현
const quic = {
  transport: "UDP",         // OS 커널의 UDP를 사용
  reliability: "자체 구현",  // 시퀀스 번호, ACK, 재전송
  encryption: "TLS 1.3 내장", // 0-RTT 연결 수립 가능
  multiplexing: true,        // 스트림 단위 독립 전송
  // TCP의 Head-of-Line Blocking 문제 해결
  advantage: "한 스트림의 패킷 유실이 다른 스트림을 막지 않음",
};

왜 TCP를 쓰지 않고 UDP 위에 새로 만들었을까요? TCP는 OS 커널에 구현되어 있어 변경이 어렵습니다. UDP는 최소한의 기능만 제공하므로, 그 위에 원하는 기능을 자유롭게 구현할 수 있습니다. QUIC는 이 자유도를 활용해 TCP의 한계 (HOL blocking, 긴 핸드셰이크) 를 해결했습니다.

다음 단계

이 글에서는 Transport 계층의 두 핵심 프로토콜인 TCP와 UDP를 비교했습니다. 다음 글에서는 데이터가 목적지를 찾아가는 과정, 즉 DNS와 IP 주소 체계를 다루겠습니다.