Transport 계층의 두 주역
이전 글에서 TCP/IP 4계층을 살펴봤습니다. 그 중 Transport 계층에는 두 가지 대표 프로토콜이 있습니다. 신뢰성을 보장하는 TCP와 속도 에 집중하는 UDP입니다.
둘 다 Application 계층의 데이터를 Internet 계층으로 전달하는 같은 역할을 맡지만, 접근 방식이 완전히 다릅니다. TCP는 "하나도 놓치지 않겠다"이고, UDP는 "빠르게 보내고 끝"입니다.
TCP: 신뢰성 있는 전송
TCP (Transmission Control Protocol) 는 연결 지향 프로토콜입니다. 데이터를 보내기 전에 반드시 연결을 수립하고, 전송 중에는 순서와 무결성을 보장하며, 끝나면 정리합니다.
3-way Handshake
TCP 연결은 세 단계로 시작됩니다.
- SYN : 클라이언트가 서버에 "연결하고 싶다"는 신호를 보냅니다. 초기 시퀀스 번호를 포함합니다.
- SYN-ACK : 서버가 "알겠다, 나도 준비됐다"고 응답합니다. 서버의 초기 시퀀스 번호와 클라이언트 시퀀스에 대한 확인을 포함합니다.
- 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가 각 단계에서 어떻게 다르게 동작하는지 비교하세요.
TCP는 데이터 전송 전 반드시 연결을 수립합니다. 클라이언트가 SYN을 보내고, 서버가 SYN-ACK로 응답하면, 클라이언트가 ACK로 확인합니다. 3번의 패킷 교환이 필요합니다.
UDP는 연결 수립 과정이 없습니다. 별도의 핸드셰이크 없이 바로 데이터를 전송할 수 있어 초기 지연이 0입니다.
TCP vs UDP 비교표
| 특성 | TCP | UDP |
|---|---|---|
| 연결 방식 | 연결 지향 (3-way handshake) | 비연결 |
| 순서 보장 | O (시퀀스 번호) | X |
| 재전송 | O (타임아웃, 중복 ACK) | X |
| 흐름 제어 | O (슬라이딩 윈도우) | X |
| 혼잡 제어 | O (Slow Start, AIMD) | X |
| 헤더 크기 | 20~60 바이트 | 8 바이트 |
| 전송 단위 | 세그먼트 (Segment) | 데이터그램 (Datagram) |
| 통신 방식 | 1:1 | 1: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 주소 체계 를 다루겠습니다.