TCP는 어떻게 신뢰성을 만드는가 — 핸드셰이크 · 흐름 제어 · 혼잡 제어
면접에서 "말로 설명한다"는 가정으로 작성. 답을 외우기보다 "왜"와 "트레이드오프"를 설명할 수 있는 것이 목표입니다.
3-1. TCP는 왜 연결을 "수립"하는가
TCP는 신뢰성 있는 연결이라 데이터를 보내기 전에 양쪽이 통신 가능한지 합의해야 합니다. 핸드셰이크의 목적은 두 가지입니다.
- 양방향 통신 확인: "내가 보낸 걸 너가 받을 수 있고, 너가 보낸 걸 내가 받을 수 있다"를 데이터 전송 전에 검증
- 시퀀스 번호 동기화: 데이터 순서를 맞추고 재전송을 판단할 기준점(ISN, 초기 시퀀스 번호)을 교환
3-2. 3-way Handshake (연결 수립)
클라이언트 서버
CLOSED LISTEN
│ ── SYN (seq=x) ──────────▶ │ SYN-SENT → SYN-RECEIVED
│ ◀── SYN-ACK (seq=y, ack=x+1) │
│ ── ACK (ack=y+1) ────────▶ │ ESTABLISHED
ESTABLISHED ESTABLISHED
- SYN: 클라이언트가 연결 요청 + 초기 시퀀스 번호
seq=x전송 - SYN-ACK: 서버가 수락(
ack=x+1) + 자신의 시퀀스 번호seq=y전송 (두 개를 하나로 합침) - ACK: 클라이언트가 서버 SYN에 확인(
ack=y+1)
ack=x+1인 이유: "x번까지 잘 받았으니 다음엔 x+1을 기대한다"는 의미. SYN은 1바이트를 소비하므로 +1.
왜 2-way가 아니라 3-way인가?
2-way면 서버가 클라이언트의 수신 능력을 확인할 수 없습니다. 클라이언트의 마지막 ACK가 있어야 "서버의 SYN이 도달했다"가 보장됩니다. 각 방향마다 "보냄 + 확인"이 필요한데, 가운데 SYN과 ACK가 합쳐지면서 4번이 3번으로 줄어든 것입니다.
ISN은 왜 랜덤하게 시작하나?
0부터 시작하면 공격자가 시퀀스 번호를 예측해 위조 패킷을 주입(TCP sequence prediction attack)하기 쉽습니다. 또한 같은 포트쌍으로 재연결될 때 이전 연결의 패킷과 섞이는 것도 방지합니다.
3-3. 4-way Handshake (연결 종료)
클라이언트(능동) 서버(수동)
│ ── FIN (seq=u) ──────────▶ │ CLOSE-WAIT
│ ◀── ACK (ack=u+1) ───────── │ (남은 데이터 전송...)
FIN-WAIT-2
│ ◀── FIN (seq=w) ─────────── │ LAST-ACK
│ ── ACK (ack=w+1) ────────▶ │ CLOSED
TIME-WAIT (2MSL 대기 후 CLOSED)
- FIN: 능동 종료자가 "보낼 데이터 끝났다" 신호
- ACK: 서버가 일단 확인만 (아직 보낼 데이터 남아있을 수 있음)
- FIN: 서버도 보낼 게 끝나면 그제서야 자신의 FIN 전송
- ACK: 클라이언트가 마지막 확인
왜 종료는 4-way인가?
수립 때는 서버의 ACK와 SYN을 SYN-ACK 하나로 합칠 수 있지만, 종료 때는 한쪽이 FIN을 보내도 상대는 보내던 데이터를 마저 보내야 할 수 있습니다(half-close). 그래서 ACK와 FIN을 분리해 보내 4단계가 됩니다.
3-4. TIME-WAIT과 2MSL (최빈출)
능동 종료자는 마지막 ACK를 보낸 뒤 바로 닫지 않고 2MSL(Maximum Segment Lifetime의 2배) 동안 대기합니다.
- 마지막 ACK 유실 대비: ACK가 사라지면 서버가 FIN을 재전송하는데, 클라이언트가 이미 닫혔다면 응답할 수 없음. TIME-WAIT으로 남아있어야 재전송된 FIN에 다시 ACK 가능
- 이전 연결의 지연 패킷 차단: 같은 포트쌍으로 새 연결이 열렸을 때, 네트워크를 떠돌던 옛 패킷이 새 연결을 오염시키는 것 방지. MSL은 패킷의 최대 생존 시간이라 2MSL을 기다리면 옛 패킷이 모두 소멸
실무 포인트
- 서버에 TIME-WAIT 소켓이 과도하게 쌓이는 문제 → 보통 능동 종료를 클라이언트가 하도록 설계,
SO_REUSEADDR옵션으로 완화 CLOSE-WAIT이 쌓이는 건 애플리케이션이 소켓을close()안 해서 생기는 버그 신호
3-5. SYN Flood 공격
공격자가 SYN만 잔뜩 보내고 마지막 ACK를 보내지 않으면, 서버는 SYN-RECEIVED 상태의 half-open 연결을 백로그 큐에 쌓아두다 고갈됩니다. 방어책으로 SYN Cookie(상태를 저장하지 않고 ACK 검증)가 있습니다.
면접 멘트: "TCP는 데이터 전송 전에 3-way handshake로 양방향 통신 가능 여부를 확인하고 시퀀스 번호를 동기화합니다. 종료는 half-close 때문에 ACK와 FIN을 분리해 4-way가 되고, 능동 종료자는 마지막 ACK 유실과 지연 패킷에 대비해 2MSL 동안 TIME-WAIT 상태로 남습니다."
3-6. 흐름 제어 vs 혼잡 제어 (반드시 구별)
연결이 살아있는 동안 "얼마나 빠르게 보낼지"를 두 메커니즘이 조율합니다. 실제 송신량은 둘 중 작은 값으로 결정됩니다: 전송량 = min(rwnd, cwnd).
| 구분 | 흐름 제어 (Flow Control) | 혼잡 제어 (Congestion Control) |
|---|---|---|
| 보호 대상 | 수신자 (받는 쪽 버퍼) | 네트워크 (중간 경로 전체) |
| 막으려는 것 | 수신 버퍼 오버플로 | 라우터·링크 과부하 |
| 수단 | 슬라이딩 윈도우 (rwnd 광고) | cwnd 조절 (슬로 스타트, AIMD) |
| 누가 정하나 | 수신자가 광고 | 송신자가 스스로 추정 |
한 문장 구별: 흐름 제어는 "받는 쪽이 감당 못 할까 봐", 혼잡 제어는 "네트워크가 막힐까 봐" 속도를 조절합니다.
3-7. 흐름 제어 — 슬라이딩 윈도우
수신자는 ACK를 보낼 때마다 "내 수신 버퍼에 이만큼 여유 있다"는 값(rwnd)을 헤더에 실어 광고합니다. 송신자는 이 윈도우 크기만큼만 ACK 없이 연속 전송할 수 있고, ACK를 받으며 윈도우가 오른쪽으로 미끄러집니다.
- Silly Window Syndrome: 수신 윈도우가 아주 작은 값으로 자주 광고되어 작은 패킷이 비효율적으로 오가는 현상 → Nagle 알고리즘(송신 측), Clark's solution(수신 측)으로 완화
- Zero Window + Window Probe: 윈도우가 0이 되면 송신자가 멈췄다가, 주기적으로 probe를 보내 윈도우가 열렸는지 확인 후 재개
3-8. 혼잡 제어 — 4단계
송신자는 네트워크 상태를 직접 알 수 없어 **혼잡 윈도우(cwnd)**를 두고 패킷 손실·지연을 단서로 추정합니다.
- 슬로 스타트(Slow Start): cwnd를 작게 시작해 ACK마다 지수적으로 증가(매 RTT 2배). 이름과 달리 증가 자체는 빠름
- 혼잡 회피(Congestion Avoidance): 임계값(ssthresh) 도달 후 RTT당 1 MSS씩 선형 증가
- 타임아웃 발생: 심각한 혼잡으로 보고 cwnd를 1로 떨어뜨리고 슬로 스타트부터 재시작
- 3 중복 ACK(빠른 재전송 + 빠른 회복): 가벼운 손실로 보고 해당 패킷 즉시 재전송, cwnd를 절반만 줄이고 혼잡 회피 지속
AIMD
"손실 시 절반으로(Multiplicative Decrease), 평소엔 조금씩(Additive Increase)" 패턴이 TCP 혼잡 제어의 핵심 철학. 그래프로 그리면 톱니(sawtooth) 모양.
알고리즘 변종
- Reno / NewReno: 손실 기반
- CUBIC: 리눅스 기본, 고대역폭 환경에 최적화
- BBR: 구글. 손실이 아니라 대역폭·RTT를 직접 추정
3-9. TCP vs UDP
| 속성 | TCP | UDP |
|---|---|---|
| 연결성 | 연결 지향(핸드셰이크) | 비연결 |
| 신뢰성 | 보장(ACK·재전송) | 없음 |
| 순서 보장 | 보장 | 없음 |
| 속도·헤더 | 느림 · 20바이트+ | 빠름 · 8바이트 |
| 제어 기능 | 흐름·혼잡 제어 있음 | 없음 |
| 사용 사례 | 웹·이메일·파일전송(HTTP, SMTP, FTP) | 실시간·스트리밍(DNS, 게임, VoIP) |
핵심은 "신뢰성 vs 속도" 트레이드오프입니다.
UDP는 신뢰성이 없는데 왜 쓰나?
재전송이 오히려 해로운 경우가 있기 때문입니다. 실시간 화상통화에서 0.5초 전 프레임을 재전송받아봐야 이미 늦었고, 차라리 버리고 다음 걸 받는 게 낫습니다. 또 DNS처럼 요청-응답이 한 번에 끝나는 작은 거래는 연결 수립 비용이 아까워 UDP를 씁니다.
QUIC (HTTP/3가 UDP 위에 올라간 이유)
TCP의 신뢰성·순서보장·혼잡제어를 UDP 위에서 애플리케이션 레벨로 재구현. TCP는 커널에 박혀 있어 개선이 느리고 핸드셰이크 지연(TCP+TLS 2RTT)이 큰데, QUIC은 연결+암호화를 통합해 1RTT(재연결 0-RTT)로 줄이고 HOL 블로킹도 해결합니다.
면접 멘트: "흐름 제어는 수신자 버퍼를 보호하려 rwnd로 속도를 맞추고, 혼잡 제어는 네트워크를 보호하려 cwnd를 슬로 스타트와 AIMD로 조절합니다. 실제 전송량은 둘 중 작은 값입니다. UDP는 신뢰성을 포기하는 대신 재전송이 해로운 실시간 환경에서 유리하고, QUIC은 그 UDP 위에서 TCP의 장점을 다시 구현한 것입니다."