웹 페이지를 열거나 파일을 다운로드할 때, 데이터가 전송되기도 전에 눈에 보이지 않는 사전 작업이 이루어집니다. 바로 3-Way Handshake입니다. 한국어로 하면 “세 번의 악수”인데, 왜 두 번도 네 번도 아닌 정확히 세 번이어야 할까요? 이 질문은 CS 기술 면접의 단골이기도 하지만, 그 이상으로 네트워크 통신의 신뢰성이 어떻게 보장되는지를 꿰뚫는 핵심 질문입니다. SYN 패킷 하나가 인터넷을 건너는 순간부터 연결이 확립되기까지의 모든 과정을, 지금부터 물리적 원리와 실무 코드까지 함께 완전히 정복합니다.
목차
- 왜 연결 설정이 필요한가 — TCP와 UDP의 근본적 차이
- 3-Way Handshake의 단계별 완전 해부
- 세 번이어야 하는 수학적 이유 — 2번으로는 왜 안 되는가
- 3-Way Handshake의 보안 취약점과 공격 유형
- 실무에서 만나는 3-Way Handshake — 소켓·레이턴시·최적화
- 전문가 관점: 4-Way Handshake·TLS·HTTP/3와의 관계
1. 왜 연결 설정이 필요한가 — TCP와 UDP의 근본적 차이 {#1}
3-Way Handshake를 이해하려면 먼저 “왜 연결 설정 과정이 필요한가”라는 질문에 답해야 합니다. 모든 네트워크 프로토콜이 이런 과정을 거치는 것은 아니기 때문입니다.
인터넷 패킷의 냉혹한 현실
인터넷에서 데이터는 **패킷(Packet)**이라는 작은 조각으로 분리되어 전송됩니다. 각 패킷은 출발지에서 목적지까지 라우터들을 거쳐 독립적으로 이동합니다. 이 과정에서 현실적으로 다음 일들이 일어날 수 있습니다.
[인터넷에서 패킷에게 일어날 수 있는 일들]
패킷 A ──[라우터1]──[라우터2]──[라우터3]──► 목적지
│
├── 지연(Delay): 혼잡으로 큐에서 대기
├── 손실(Loss): 라우터 버퍼 오버플로우로 드롭
├── 순서 바뀜(Reorder): 다른 경로로 우회해 늦게 도착
├── 중복(Duplicate): 네트워크 장비 오류로 복사본 생성
└── 변조(Corruption): 물리 레이어 오류로 비트 변경
이러한 불신뢰적인 환경 위에서 신뢰할 수 있는 통신을 만드는 것이 **TCP(Transmission Control Protocol)**의 임무입니다.
TCP vs UDP: 신뢰성의 비용
[TCP와 UDP 핵심 비교]
┌────────────────┬──────────────────────┬──────────────────────┐
│ 특성 │ TCP │ UDP │
├────────────────┼──────────────────────┼──────────────────────┤
│ 연결 설정 │ 3-Way Handshake 필요 │ 없음 (연결 없음) │
│ 신뢰성 │ 보장 (ACK + 재전송) │ 미보장 │
│ 순서 보장 │ 보장 (시퀀스 번호) │ 미보장 │
│ 흐름 제어 │ 있음 (슬라이딩 윈도우)│ 없음 │
│ 혼잡 제어 │ 있음 (AIMD 등) │ 없음 │
│ 오버헤드 │ 큼 (헤더 20~60 byte) │ 작음 (헤더 8 byte) │
│ 속도 │ 상대적으로 느림 │ 빠름 │
│ 주요 사용처 │ HTTP, HTTPS, SSH, │ DNS, 스트리밍, │
│ │ 파일 전송, 이메일 │ 게임, VoIP, QUIC │
└────────────────┴──────────────────────┴──────────────────────┘
UDP는 편지를 그냥 우편함에 넣는 것과 같습니다. 빠르지만 상대방이 받았는지 확인하지 않습니다. TCP는 등기 우편과 같습니다. 느리지만 수신 확인을 받고, 분실되면 다시 보냅니다. 3-Way Handshake는 이 “등기 우편 시스템”을 시작하기 위해 수신자가 준비되었는지 확인하는 과정입니다.
TCP가 보장해야 하는 두 가지 핵심
3-Way Handshake가 해결해야 하는 문제는 정확히 두 가지입니다.
① 양방향 통신 능력 확인 (Bidirectional Communication Verification)
인터넷에서 통신은 양방향입니다. 클라이언트→서버 방향과 서버→클라이언트 방향이 모두 정상인지 확인해야 합니다. 한쪽 방향만 열려 있어서는 신뢰할 수 있는 통신이 불가능합니다.
② 초기 시퀀스 번호 동기화 (ISN Synchronization)
TCP는 모든 바이트에 번호를 매겨 순서를 관리합니다. 이 번호의 출발점, 즉 **ISN(Initial Sequence Number)**을 양쪽이 서로 교환하고 확인해야 이후 데이터 전송이 올바르게 추적될 수 있습니다.
2. 3-Way Handshake의 단계별 완전 해부 {#2}
이제 3-Way Handshake의 각 단계를 패킷 수준까지 완전히 분해합니다.
TCP 헤더의 핵심 필드
3-Way Handshake를 이해하려면 TCP 헤더의 주요 필드를 먼저 알아야 합니다.
[TCP 헤더 구조 (주요 필드)]
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├─────────────────────────┼─────────────────────────────────────────┤
│ Source Port │ Destination Port │
├─────────────────────────────────────────────────────────────────┤
│ Sequence Number (32bit) │
├─────────────────────────────────────────────────────────────────┤
│ Acknowledgment Number (32bit) │
├───────────┬─────────────┬───────────────────────────────────────┤
│ Data │ Reserved │ U│A│P│R│S│F│ │
│ Offset │ │ R│C│S│S│Y│I│ Window Size │
│ │ │ G│K│H│T│N│N│ │
└───────────┴─────────────┴──┴─┴─┴─┴─┴─┴───────────────────────┘
핵심 플래그:
SYN (Synchronize) : 연결 요청 / 시퀀스 번호 동기화
ACK (Acknowledge) : 수신 확인 (ACK 번호가 유효함을 표시)
FIN (Finish) : 연결 종료 요청
RST (Reset) : 연결 강제 리셋
핵심 필드:
Sequence Number : 이 패킷의 첫 번째 바이트 번호
ACK Number : 다음에 받기를 기대하는 바이트 번호
Window Size : 수신 버퍼 여유 공간 (흐름 제어용)
STEP 1 — SYN: “통신할 수 있나요? 내 번호는 X입니다”
클라이언트가 서버에 연결을 요청합니다.
[STEP 1: SYN 패킷]
클라이언트 서버
│ │
│ TCP 패킷: │
│ SYN = 1 │
│ ACK = 0 │
│ Seq = x (ISN_client, 예: 1000) │
│ ACK_Num = 0 (아직 받은 것 없음) │
│ │
│ ──────────────── SYN ────────────────────────►│
│ │
│ 상태: SYN_RCVD │
클라이언트 상태: SYN_SENT
ISN(Initial Sequence Number)은 왜 랜덤인가?
ISN이 항상 0부터 시작하면 공격자가 시퀀스 번호를 예측해 가짜 패킷을 주입할 수 있습니다. 또한 이전 연결의 지연된 패킷이 새 연결에 섞이는 것을 막기 위해 ISN은 운영체제가 암호학적으로 안전한 난수로 생성합니다. RFC 6528에서는 시간과 연결별 해시값을 조합하는 방식을 권고합니다.
Linux ISN 생성 방식 (개념적):
ISN = MD5(src_ip, src_port, dst_ip, dst_port, 비밀키) + 타이머값
→ 외부에서 예측 불가능, 연결마다 고유한 값
STEP 2 — SYN-ACK: “들렸어요. 내 번호는 Y입니다, 당신 번호 확인”
서버가 클라이언트의 요청을 수신했음을 확인하고, 자신의 ISN을 함께 알립니다.
[STEP 2: SYN-ACK 패킷]
클라이언트 서버
│ │
│ TCP 패킷: │
│ SYN = 1 │
│ ACK = 1 │
│ Seq = y (ISN_server, 예: 5000)
│ ACK_Num = x+1 (= 1001)
│ ← 클라이언트 ISN+1 확인
│ │
│◄─────────────────── SYN-ACK ─────────────────┤
│ │
클라이언트 상태: 아직 SYN_SENT
서버 상태: SYN_RCVD
SYN-ACK 패킷이 하는 두 가지 일:
1. ACK(ACK_Num = x+1): "클라이언트의 x번 받음, 다음은 x+1 보내주세요"
2. SYN(Seq = y): "내 ISN은 y입니다, 확인해주세요"
ACK_Num = Seq + 1인 이유
SYN 패킷 자체는 데이터가 없지만 TCP 규약상 시퀀스 번호를 1 소비합니다. 따라서 “x번 받았고 다음은 x+1 주세요”라는 의미로 ACK_Num = x+1이 됩니다. FIN 패킷도 같은 이유로 시퀀스 번호를 1 소비합니다.
STEP 3 — ACK: “당신 번호도 확인했습니다. 이제 시작하죠”
클라이언트가 서버의 SYN-ACK를 수신 확인합니다. 이 순간 연결이 완전히 수립됩니다.
[STEP 3: ACK 패킷]
클라이언트 서버
│ │
│ TCP 패킷: │
│ SYN = 0 │
│ ACK = 1 │
│ Seq = x+1 (= 1001) │
│ ACK_Num = y+1 (= 5001) │
│ ← 서버 ISN+1 확인 │
│ │
│ ──────────────── ACK ────────────────────────►│
│ │
클라이언트 상태: ESTABLISHED 서버 상태: ESTABLISHED
│ │
│ ══════════ 데이터 전송 가능 ══════════════════│
전체 흐름 한눈에 보기
[3-Way Handshake 완전 시퀀스 다이어그램]
클라이언트 네트워크 서버
│ │ │
│ ① SYN │ │
│ Seq=1000, SYN=1 │ │
│─────────────────────────────────────────► │
│ │ 상태: SYN_RCVD
│ │ ② SYN-ACK │
│ │ Seq=5000, ACK=1001 │
│◄───────────────────────────────────────── │
│ 상태: ESTABLISHED │
│ ③ ACK │ │
│ Seq=1001, ACK=5001 │ │
│─────────────────────────────────────────► │
│ │ 상태: ESTABLISHED
│ │
│ ══════════════ 데이터 전송 시작 ═══════════│
│ HTTP GET /index.html │
│─────────────────────────────────────────► │
│ │
│ HTTP 200 OK + HTML 데이터 │
│◄───────────────────────────────────────── │
각 단계별 검증 내용:
① SYN: 클라이언트 → 서버 전송 가능 확인 (단방향)
② SYN-ACK: 서버 → 클라이언트 전송 가능 확인 + 서버 ISN 전달
③ ACK: 클라이언트의 수신 능력 확인 + 서버 ISN 동기화 완료
→ 양방향 모두 확인 완료 → ESTABLISHED
3. 세 번이어야 하는 수학적 이유 — 2번으로는 왜 안 되는가 {#3}
3-Way Handshake에서 가장 핵심적인 질문이 바로 이것입니다. “왜 정확히 세 번인가?” 이 질문에 답하는 것이 프로토콜 설계의 본질을 이해하는 일입니다.
2-Way로 충분하지 않은 이유
“SYN → SYN-ACK, 두 번으로 연결 설정하면 안 되나요?”라는 의문은 매우 자연스럽습니다. 2-Way Handshake를 시도하면 어떤 문제가 생기는지 살펴봅니다.
[2-Way Handshake의 치명적 문제]
시나리오: 오래된 SYN 패킷이 뒤늦게 도착하는 경우
시간축 ──────────────────────────────────────────►
t=0: 클라이언트가 서버에 SYN_A 전송
t=1: SYN_A가 네트워크에서 지연(소실된 줄 알고 재전송)
t=2: 클라이언트가 SYN_B 재전송 → 정상 연결 수립 → 사용 → 종료
t=3: SYN_A가 뒤늦게 서버에 도착!
[2-Way 방식이라면]
서버: SYN_A 수신 → SYN-ACK 전송 → 서버 측 연결 ESTABLISHED
클라이언트: 이미 종료된 연결 → SYN-ACK 무시
결과: 서버만 연결 상태, 클라이언트는 연결 없음
→ 서버 리소스(메모리, 포트) 불필요하게 점유
→ 고아 연결(Half-Open Connection) 발생
[3-Way 방식이라면]
서버: SYN_A 수신 → SYN-ACK 전송 → ACK 대기 (SYN_RCVD 상태)
클라이언트: 이미 종료된 연결 → ACK 보내지 않음
서버: ACK 미수신 → 타임아웃 후 연결 자원 해제
결과: 서버가 스스로 정리 → 고아 연결 방지
3번째 ACK가 바로 이 문제를 해결합니다. 클라이언트가 ACK를 보내야만 서버가 ESTABLISHED 상태로 전환하므로, 오래된 SYN 패킷이 불필요한 연결을 만드는 것을 방지합니다.
양방향 확인의 논리적 최솟값
두 호스트가 양방향 통신 능력을 서로 확인하려면 최소 몇 번의 메시지가 필요한지 논리적으로 따져봅니다.
[양방향 확인에 필요한 최소 메시지 수 분석]
확인해야 할 사실:
① 클라이언트 → 서버 전송 가능 (클라이언트의 송신 + 서버의 수신)
② 서버 → 클라이언트 전송 가능 (서버의 송신 + 클라이언트의 수신)
③ 양쪽 ISN 동기화 완료
메시지별 확인 가능한 내용:
─────────────────────────────────────────────────
메시지 1 (C→S: SYN)
확인: 클라이언트가 송신할 수 있다
확인: 서버가 수신할 수 있다
미확인: 서버가 송신할 수 있는지 (②)
미확인: 클라이언트가 수신할 수 있는지 (②)
메시지 2 (S→C: SYN-ACK)
확인: 서버가 송신할 수 있다 ← ① 완전 확인
확인: 클라이언트가 수신할 수 있다
미확인: 서버가 ACK를 받았는지 (서버 ISN 동기화 미완료)
메시지 3 (C→S: ACK)
확인: 클라이언트의 SYN-ACK 수신 능력 확인 ← ② 완전 확인
확인: 서버의 ISN 동기화 완료 ← ③ 완전 확인
─────────────────────────────────────────────────
결론: 3번의 메시지가 논리적 최솟값
4번으로 늘리면 더 안전하지 않은가
“그럼 4번, 5번으로 늘리면 더 안전한가요?”라는 질문에 대한 답도 명확합니다. 3-Way Handshake 이후 추가적인 확인 메시지를 교환한다 해도 네트워크의 불확실성을 완전히 제거할 수 없습니다. 마지막 메시지가 항상 손실될 가능성이 있기 때문입니다. 이것은 **두 장군 문제(Two Generals Problem)**라는 분산 시스템의 고전적 난제와 연결됩니다.
[두 장군 문제: 완벽한 합의는 불가능하다]
상황: 두 장군이 적의 도시를 협동 공격하려 합니다.
전령은 적진을 통과해야 하므로 언제든 잡힐 수 있습니다.
장군 A → (전령이 잡힐 수 있음) → 장군 B: "내일 공격하자"
장군 B → (전령이 잡힐 수 있음) → 장군 A: "알겠다"
장군 A → (전령이 잡힐 수 있음) → 장군 B: "네 응답 받았다"
...
→ 마지막 메시지가 도달했는지 상대방은 결코 확신할 수 없음
→ 메시지를 무한히 늘려도 완전한 확실성 달성 불가
TCP의 현실적 해법:
3번의 교환으로 "충분히 높은 신뢰도"를 달성하고
나머지 불확실성은 재전송 타임아웃으로 처리
3번은 “완벽한 확실성”이 아니라 “실용적 최솟값”입니다. 이것이 3-Way Handshake가 수십 년간 인터넷의 표준으로 유지되는 이유입니다.
4. 3-Way Handshake의 보안 취약점과 공격 유형 {#4}
3-Way Handshake의 설계를 이해하면 이를 악용하는 공격 방식도 자연스럽게 이해됩니다.
SYN Flood 공격: 3번째 ACK를 보내지 않는다
SYN Flood는 가장 고전적인 TCP 기반 DDoS 공격입니다. 3-Way Handshake의 구조적 특성을 정확히 노립니다.
[SYN Flood 공격 메커니즘]
공격자 (봇넷) 서버
│ │
│ SYN (IP 위조: 1.2.3.4) │
│──────────────────────────────────────────────►│
│ │ SYN_RCVD 상태
│ │ Backlog Queue에 저장
│ SYN (IP 위조: 1.2.3.5) │
│──────────────────────────────────────────────►│
│ SYN (IP 위조: 1.2.3.6) (수만~수백만 개) │
│──────────────────────────────────────────────►│
│ │ │
│ │ SYN-ACK 전송 │
│ │ (위조 IP로 │
│ │ → 응답 없음) │
│ │ │
│ ▼ │
│ Backlog Queue 포화 │
│ (정상 연결 요청 처리 불가) │
│ │
│ 합법적인 클라이언트 │
│──────────────────── SYN ────────────────────►│
│ ✗ 큐 꽉 참, 거부 │
결과: 서버가 정상 연결 요청을 처리 불가 → 서비스 거부(DoS)
SYN Flood 대응 기법들:
① SYN Cookie (가장 대표적 방어)
원리: SYN_RCVD 상태로 큐에 저장하는 대신,
시퀀스 번호 자체에 연결 정보를 인코딩
└─ SYN-ACK의 Seq = Hash(src_ip, src_port, dst_ip, dst_port, 타임스탬프)
└─ 진짜 클라이언트가 ACK를 보내면 그 안의 번호로 연결 복원
└─ 가짜 IP는 ACK가 안 옴 → 서버 메모리 0 소비
# Linux에서 SYN Cookie 활성화
sysctl -w net.ipv4.tcp_syncookies=1
② 백로그 큐 크기 증가
sysctl -w net.ipv4.tcp_max_syn_backlog=65536
③ SYN-ACK 재전송 횟수 제한
sysctl -w net.ipv4.tcp_synack_retries=2 # 기본값 5 → 2로 줄임
④ Rate Limiting (iptables)
iptables -A INPUT -p tcp --syn -m limit \
--limit 100/s --limit-burst 200 -j ACCEPT
iptables -A INPUT -p tcp --syn -j DROP
TCP 시퀀스 번호 예측 공격
ISN이 예측 가능한 경우, 공격자가 연결에 끼어들 수 있습니다.
[시퀀스 번호 예측 공격 시나리오]
공격자가 알고 있는 것:
- 클라이언트 IP, 서버 IP, 서버 포트
- ISN 생성 패턴 (예측 가능한 경우)
공격 순서:
1. 공격자가 서버로 여러 연결을 맺어 ISN 패턴 분석
2. 클라이언트인 척 IP 스푸핑으로 SYN 전송
3. 서버의 SYN-ACK는 진짜 클라이언트에게 가지만
공격자가 ACK 번호를 예측해 ACK 패킷 위조
4. 서버는 공격자의 가짜 연결을 신뢰
방어:
→ 암호학적으로 안전한 난수 ISN 생성 (RFC 6528)
→ TCP 타임스탬프 옵션 활성화
→ TLS 사용 (애플리케이션 계층 암호화)
Half-Open Connection 악용
bash
# 현재 서버의 TCP 연결 상태 확인
ss -tn state syn-recv # SYN_RCVD 상태 연결 수
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
# netstat으로 상태별 집계
netstat -ant | awk '{print $6}' | sort | uniq -c | sort -rn
# 출력 예시 (SYN Flood 공격 중):
# 50000 SYN_RECV ← 비정상적으로 많은 Half-Open
# 120 ESTABLISHED
# 45 TIME_WAIT
5. 실무에서 만나는 3-Way Handshake — 소켓·레이턴시·최적화 {#5}
3-Way Handshake는 이론이 아니라 여러분이 작성하는 코드 아래에서 매 순간 실행되고 있습니다.
소켓 API와 3-Way Handshake의 대응 관계
java
// Java 소켓 프로그래밍과 3-Way Handshake 대응
// ═══ 서버 쪽 ═══
ServerSocket serverSocket = new ServerSocket(8080);
// → OS: bind() + listen() 시스템 콜
// → 서버가 SYN 받을 준비 완료 (LISTEN 상태)
Socket clientSocket = serverSocket.accept();
// → OS: accept() 시스템 콜 → 블록 대기
// → 클라이언트 SYN 수신 시 SYN-ACK 자동 전송 (OS가 처리)
// → 클라이언트 ACK 수신 시 accept() 반환 (ESTABLISHED)
// ↑ 이 한 줄 안에서 3-Way Handshake 완료!
InputStream in = clientSocket.getInputStream();
OutputStream out = clientSocket.getOutputStream();
// 이후 데이터 송수신
// ═══ 클라이언트 쪽 ═══
Socket socket = new Socket("example.com", 8080);
// → OS: connect() 시스템 콜
// → SYN 전송 → SYN-ACK 수신 → ACK 전송 (3-Way Handshake)
// → connect() 반환 시 ESTABLISHED 완료
// ↑ 이 한 줄 안에서 3-Way Handshake 완료!
python
# Python 소켓 예시 (더 명시적)
import socket
# 서버
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 8080))
server.listen(128) # 128 = 백로그 큐 크기 (SYN_RCVD 상태 최대 수)
conn, addr = server.accept() # 3-Way Handshake 완료 시 반환
print(f"연결 수립: {addr}") # ('192.168.1.100', 54321)
# 클라이언트
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('example.com', 8080)) # 3-Way Handshake 수행
3-Way Handshake가 레이턴시에 미치는 영향
3-Way Handshake는 데이터 전송 전에 반드시 **1.5 RTT(Round-Trip Time)**를 소비합니다.
[RTT와 핸드셰이크 레이턴시]
RTT = 패킷이 왕복하는 데 걸리는 시간
서울 ↔ 서울 서버: RTT ≈ 1ms
서울 ↔ 미국 서버: RTT ≈ 150ms
서울 ↔ 유럽 서버: RTT ≈ 280ms
3-Way Handshake 비용:
SYN 전송 → SYN-ACK 수신: 0.5 RTT
ACK 전송 + 첫 데이터 전송: 0.5 RTT
────────────────────────────
최소 1 RTT를 데이터 없이 소비
서울→미국 HTTP 연결 시간:
TCP Handshake: 150ms × 1 = 150ms
TLS Handshake: 150ms × 2 = 300ms (TLS 1.2)
HTTP 요청/응답: 150ms × 1 = 150ms
────────────────────────────────
총계: 600ms (첫 바이트까지!)
Connection Pool: 핸드셰이크 비용 줄이기
매 요청마다 새 TCP 연결을 맺으면 핸드셰이크 비용이 누적됩니다. Connection Pool은 연결을 미리 맺어두고 재사용합니다.
java
// HikariCP Connection Pool 설정 (Spring Boot)
// 3-Way Handshake를 minimumIdle 수만큼 미리 완료해 대기
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://db-server:3306/mydb");
// 핵심 설정: 연결 풀 크기
config.setMinimumIdle(10); // 최소 10개 연결을 항상 ESTABLISHED 유지
config.setMaximumPoolSize(50);// 최대 50개까지 확장
// 연결 유효성 검사 (TCP 연결이 끊어졌는지 확인)
config.setConnectionTestQuery("SELECT 1");
config.setKeepaliveTime(30_000); // 30초마다 keepalive 패킷
// 연결 타임아웃 (3-Way Handshake 포함)
config.setConnectionTimeout(30_000); // 30초
config.setIdleTimeout(600_000); // 10분 유휴 시 반환
return new HikariDataSource(config);
}
TCP Fast Open (TFO): 핸드셰이크와 데이터를 동시에
[TCP Fast Open 동작 방식]
일반 TCP:
SYN ──────────────────► 서버
◄────────────── SYN-ACK
ACK + HTTP GET ────────► 서버 ← 1 RTT 후 데이터 전송
◄────────────── HTTP 200
TCP Fast Open:
최초 연결: 일반 3-Way + 쿠키 발급
이후 재연결:
SYN + HTTP GET + TFO Cookie ──► 서버 ← 핸드셰이크와 동시에 데이터!
◄──────────── SYN-ACK + HTTP 200 ← 0.5 RTT 절약
# Linux TFO 활성화
sysctl -w net.ipv4.tcp_fastopen=3 # 클라이언트 + 서버 모두 활성화
java
// Java HttpClient에서 HTTP Keep-Alive 활용 (핸드셰이크 재사용)
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // HTTP/2: 하나의 TCP 연결로 다중 요청
.connectTimeout(Duration.ofSeconds(10))
.build();
// 동일 HttpClient 재사용 → TCP 연결 재사용 → 핸드셰이크 1회만 수행
HttpResponse<String> res1 = client.send(request1, BodyHandlers.ofString());
HttpResponse<String> res2 = client.send(request2, BodyHandlers.ofString());
// ↑ 두 번째 요청은 핸드셰이크 없이 기존 연결 재사용
6. 전문가 관점: 4-Way Handshake·TLS·HTTP/3와의 관계 {#6}
3-Way Handshake를 완전히 이해했다면, 이와 연관된 더 넓은 개념들을 연결합니다.
4-Way Handshake: 연결을 닫는 방법
TCP 연결 종료는 연결 수립보다 한 단계 더 필요합니다. 각 방향의 연결을 독립적으로 닫기 때문입니다.
[TCP 4-Way Handshake (연결 종료)]
클라이언트 서버
│ │
│ ① FIN (Seq=u) │
│ "더 이상 보낼 데이터 없음" │
│──────────────────────────────────────────────►│ CLOSE_WAIT
│ │
│ ② ACK (ACK_Num=u+1) │
│ "FIN 받음, 잠깐만요 (아직 보낼 것 있을 수도)│
│◄─────────────────────────────────────────────┤
│ FIN_WAIT_2 │
│ (서버가 남은 데이터 전송) │
│ │
│ ③ FIN (Seq=v) │
│ "이제 나도 다 보냈음" │
│◄─────────────────────────────────────────────┤ LAST_ACK
│ │
│ ④ ACK (ACK_Num=v+1) │
│──────────────────────────────────────────────►│ CLOSED
│ TIME_WAIT (2×MSL 대기) │
│ │
3-Way와 달리 4-Way인 이유:
→ SYN-ACK처럼 FIN+ACK를 동시에 못 보내는 경우가 있음
(서버가 FIN 받은 시점에 아직 보낼 데이터가 남아 있을 수 있음)
→ 각 방향의 종료를 독립적으로 처리 = Half-Close 지원
TIME_WAIT 상태:
→ 마지막 ACK가 유실될 경우를 대비해 2×MSL(Maximum Segment Lifetime)
만큼 대기 (Linux 기본값: 약 60초)
→ 이 기간 동안 동일 포트 재사용 불가
→ 고빈도 단기 연결 서버에서 포트 고갈 문제 유발 가능
TLS Handshake: 3-Way 위에 올라타는 보안 계층
HTTPS는 TCP 3-Way Handshake 완료 후 TLS Handshake를 추가로 수행합니다.
[HTTPS 연결 수립 전체 과정]
클라이언트 서버
│ │
│ ① TCP SYN │
│─────────────────────────────────────►│
│ ② TCP SYN-ACK │
│◄─────────────────────────────────────┤
│ ③ TCP ACK │ TCP 완료 (1 RTT)
│─────────────────────────────────────►│
│ │
│ ④ TLS ClientHello │
│ (지원 암호 목록, 난수, 세션ID) │
│─────────────────────────────────────►│
│ ⑤ TLS ServerHello + Certificate │
│ + ServerHelloDone │
│◄─────────────────────────────────────┤ TLS 1.2: 2 RTT 추가
│ ⑥ ClientKeyExchange + ChangeCipher │
│ + Finished │
│─────────────────────────────────────►│
│ ⑦ ChangeCipherSpec + Finished │
│◄─────────────────────────────────────┤
│ │
│ ⑧ HTTP 요청/응답 (암호화) │ 총 3 RTT 소비
│─────────────────────────────────────►│
TLS 1.3 개선:
→ 2 RTT → 1 RTT로 단축 (HandShake 간소화)
→ 0-RTT (Early Data): 세션 재개 시 첫 요청과 함께 데이터 전송
HTTP/3와 QUIC: 3-Way Handshake를 없애다
HTTP/3는 TCP 대신 QUIC(Quick UDP Internet Connections) 프로토콜을 사용합니다. QUIC은 UDP 위에 신뢰성과 암호화를 직접 구현해 3-Way Handshake의 지연을 획기적으로 줄입니다.
[HTTP/1.1 vs HTTP/2 vs HTTP/3 연결 비용 비교]
HTTP/1.1 (TCP + TLS 1.2):
TCP 3-Way: 1 RTT
TLS 1.2: 2 RTT
첫 요청: 1 RTT
─────────────────
총 4 RTT (첫 바이트까지)
HTTP/2 (TCP + TLS 1.3):
TCP 3-Way: 1 RTT
TLS 1.3: 1 RTT (병합 가능)
첫 요청: 1 RTT
─────────────────
총 2~3 RTT
HTTP/3 (QUIC):
QUIC Handshake: 1 RTT (TLS 1.3 내장)
첫 요청: 0 RTT (세션 재개 시)
─────────────────
신규: 1 RTT / 재연결: 0 RTT
QUIC의 추가 장점:
→ 패킷 손실 시 특정 스트림만 영향 (TCP의 HOL 블로킹 없음)
→ 네트워크 변경(Wi-Fi→LTE) 시 연결 유지 (Connection ID 기반)
→ 멀티플렉싱 기본 지원
결론
3-Way Handshake가 필요한 이유는 세 가지로 요약됩니다. 첫째, 양방향 통신 능력을 논리적 최솟값인 세 번의 메시지로 완전히 검증합니다. 둘째, 양쪽의 ISN을 교환하고 확인해 이후 모든 데이터 전송의 신뢰성을 보장하는 기반을 만듭니다. 셋째, 오래된 중복 SYN 패킷이 불필요한 연결을 수립하는 것을 방지해 서버 자원을 보호합니다. 오늘 배운 내용을 바탕으로 ss -tan으로 현재 서버의 TCP 연결 상태를 직접 확인하고, strace로 connect() 시스템 콜이 어떻게 3-Way Handshake를 트리거하는지 추적해보는 것부터 시작해보세요.
답글 남기기