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 주소 체계 를 다루겠습니다.