← Blog
Computer Science / 네트워크

TCP는 어떻게 신뢰성을 만드는가 — 핸드셰이크 · 흐름 제어 · 혼잡 제어

면접에서 "말로 설명한다"는 가정으로 작성. 답을 외우기보다 "왜"와 "트레이드오프"를 설명할 수 있는 것이 목표입니다.


3-1. TCP는 왜 연결을 "수립"하는가

TCP는 신뢰성 있는 연결이라 데이터를 보내기 전에 양쪽이 통신 가능한지 합의해야 합니다. 핸드셰이크의 목적은 두 가지입니다.

  1. 양방향 통신 확인: "내가 보낸 걸 너가 받을 수 있고, 너가 보낸 걸 내가 받을 수 있다"를 데이터 전송 전에 검증
  2. 시퀀스 번호 동기화: 데이터 순서를 맞추고 재전송을 판단할 기준점(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
  1. SYN: 클라이언트가 연결 요청 + 초기 시퀀스 번호 seq=x 전송
  2. SYN-ACK: 서버가 수락(ack=x+1) + 자신의 시퀀스 번호 seq=y 전송 (두 개를 하나로 합침)
  3. 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)
  1. FIN: 능동 종료자가 "보낼 데이터 끝났다" 신호
  2. ACK: 서버가 일단 확인만 (아직 보낼 데이터 남아있을 수 있음)
  3. FIN: 서버도 보낼 게 끝나면 그제서야 자신의 FIN 전송
  4. ACK: 클라이언트가 마지막 확인

왜 종료는 4-way인가?

수립 때는 서버의 ACK와 SYN을 SYN-ACK 하나로 합칠 수 있지만, 종료 때는 한쪽이 FIN을 보내도 상대는 보내던 데이터를 마저 보내야 할 수 있습니다(half-close). 그래서 ACK와 FIN을 분리해 보내 4단계가 됩니다.


3-4. TIME-WAIT과 2MSL (최빈출)

능동 종료자는 마지막 ACK를 보낸 뒤 바로 닫지 않고 2MSL(Maximum Segment Lifetime의 2배) 동안 대기합니다.

  1. 마지막 ACK 유실 대비: ACK가 사라지면 서버가 FIN을 재전송하는데, 클라이언트가 이미 닫혔다면 응답할 수 없음. TIME-WAIT으로 남아있어야 재전송된 FIN에 다시 ACK 가능
  2. 이전 연결의 지연 패킷 차단: 같은 포트쌍으로 새 연결이 열렸을 때, 네트워크를 떠돌던 옛 패킷이 새 연결을 오염시키는 것 방지. 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)**를 두고 패킷 손실·지연을 단서로 추정합니다.

  1. 슬로 스타트(Slow Start): cwnd를 작게 시작해 ACK마다 지수적으로 증가(매 RTT 2배). 이름과 달리 증가 자체는 빠름
  2. 혼잡 회피(Congestion Avoidance): 임계값(ssthresh) 도달 후 RTT당 1 MSS씩 선형 증가
  3. 타임아웃 발생: 심각한 혼잡으로 보고 cwnd를 1로 떨어뜨리고 슬로 스타트부터 재시작
  4. 3 중복 ACK(빠른 재전송 + 빠른 회복): 가벼운 손실로 보고 해당 패킷 즉시 재전송, cwnd를 절반만 줄이고 혼잡 회피 지속

AIMD

"손실 시 절반으로(Multiplicative Decrease), 평소엔 조금씩(Additive Increase)" 패턴이 TCP 혼잡 제어의 핵심 철학. 그래프로 그리면 톱니(sawtooth) 모양.

알고리즘 변종

  • Reno / NewReno: 손실 기반
  • CUBIC: 리눅스 기본, 고대역폭 환경에 최적화
  • BBR: 구글. 손실이 아니라 대역폭·RTT를 직접 추정

3-9. TCP vs UDP

속성TCPUDP
연결성연결 지향(핸드셰이크)비연결
신뢰성보장(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의 장점을 다시 구현한 것입니다."