
[기술 딥다이브] TCP 핸드셰이크는 왜 3단계인가
한 줄 요약: TCP 3-way handshake는 SYN→SYN-ACK→ACK 세 단계로 양방향 시퀀스 번호를 합의하는 절차이며, 클라이언트와 서버 양쪽의 시퀀스가 모두 확인돼야 하므로 2단계로는 부족하다.
TCP 연결을 맺을 때 SYN, SYN-ACK, ACK 세 단계를 거쳐요. 이것을 3-way handshake라고 부어요. ’왜 2단계가 아니라 3단계인가?’라는 질문을 도식과 실제 출력으로 풀어 설명하겠어요. TCP 연결 설정은 이 글의 동반 글인 HTTP/3가 UDP 위에 있는 이유를 이해하는 출발점이기도 해요.
핸드셰이크 도식: 세 패킷의 흐름
아래 도식은 클라이언트와 서버 사이를 오가는 세 패킷과 각 단계에서의 상태를 보여줘요. seq는 각 쪽이 보내는 시퀀스 번호의 시작점이고, ack는 “상대의 시퀀스를 여기까지 받았다”는 확인이에요.
SYN: 클라이언트가 ‘연결을 원한다’
첫 단계예요. 클라이언트가 서버에 SYN(동기) 패킷을 보내요. ’연결하고 싶다’는 뜻이에요. 이때 클라이언트는 자신의 시퀀스 번호(예: x)를 함께 보내요. 이 번호는 ’내 데이터가 여기부터 시작한다’는 기준점이에요. 이 값을 ISN(Initial Sequence Number, 초기 시퀀스 번호)이라고 부르며, 매 연결마다 무작위에 가깝게 골라요. 0부터 시작하지 않는 이유는 이전 연결의 늦게 도착한 패킷이 새 연결에 섞여 들어오는 일을 막기 위해서예요.
SYN-ACK: 서버가 ‘승인하며, 나도 준비됐다’
서버가 SYN을 받으면 SYN-ACK 패킷으로 응답해요. ’연결을 수락하며, 나도 준비됐다’는 뜻이에요. 서버도 자신의 시퀀스 번호(예: y)를 보내고, 클라이언트의 x를 받았다는 확인(ack = x+1)을 함께 담아요. ack 값이 x가 아니라 x+1인 이유는, SYN이 ’데이터 1바이트를 소비한다’고 약속되어 있기 때문이에요.
ACK: 클라이언트가 ’확인했다’고 마무리
클라이언트가 SYN-ACK를 받으면 ACK 패킷을 보내요. 서버의 시퀀스 y를 받았다는 확인(ack = y+1)예요. 이 ACK가 도착하면 양쪽 모두 ESTABLISHED 상태가 되어 데이터를 주고받을 수 있어요.
왜 2단계로는 부족한가
핵심 질문이에요. 2단계(SYN, SYN-ACK)만으로도 ’연결하고 싶다’는 의사를 교환하지 않느냐고 생각할 수 있어요. 그러나 2단계는 클라이언트→서버 방향만 동기화돼요. 서버의 시퀀스 y가 클라이언트에게 무사히 도달했는지 확인되지 않아요.
세 번째 ACK가 필요한 이유는 서버→클라이언트 방향의 동기화를 확인하기 위해서예요. 무전기로 한쪽만 ’들었다’고 끝내면 상대가 들었는지 서로 모르듯, 시작점이 서로에게 닿았다는 확인이 쌍으로 필요해요. 세 단계를 거쳐야 양방향 모두 시퀀스가 합의되고, 양쪽 모두 데이터를 받을 준비가 완료됐음을 확신할 수 있어요.
왜 4단계도 아닌가
“그러면 양방향이니 4단계가 돼야 하는 것 아닌가?“라는 역질문도 가능해요. 정답은 세 번째 ACK가 두 가지 일을 동시에 한다는 것이에요. (1) 서버의 y를 확인하고, (2) 이 패킷 자체에 데이터를 실어 보낼 수 있어요. TCP는 ACK를 ’빈 확인’이 아니라 데이터 패킷에 끼워 보내는 지연 ACK·piggyback 구조이므로, 굳이 네 번째 패킷을 별도로 보낼 필요가 없어요. 그래서 3단계로 충분해요.
상태머신: 연결이 거치는 단계
TCP 연결은 명확한 상태를 거쳐요. 클라이언트는 SYN을 보낸 뒤 SYN_SENT에 머물고, 서버는 평소 LISTEN에서 SYN을 받아 SYN_RECEIVED로 옮겨가요. 마지막 ACK가 양쪽에 도달하면 둘 다 ESTABLISHED가 돼요. 이 상태 전환은 운영체제 커널이 관리하므로, 애플리케이션은 ’연결이 열렸다’는 결과만 받아요. 핸드셰이크가 보이지 않는 이유예요.
실제로 확인해 보기: 커넥션 한 줄
패킷 단위의 SYN/SYN-ACK/ACK는 일반 사용자 권한으로는 직접 잡기 어렵지만, 핸드셰이크가 끝난 결과는 curl -v로 바로 확인할 수 있어요. 아래는 example.com에 80번 포트로 연결한 실제 출력이에요.
* Trying [2606:4700:10::6814:179a]:80...
* Established connection to example.com (2606:4700:10::6814:179a port 80)
from 2001:4430:c25c:6c2c:... port 37566
Trying에서 목적지 IP로 SYN을 보내고, Established connection이 뜨는 순간이 바로 3-way handshake가 완료된 시점이에요. 클라이언트 쪽 포트 37566이 임시로 배정된 ephemeral port(잠시 쓰고 버리는 포트)이고, 이 포트와 서버 포트 80 사이에 연결이 성립해요. 한 줄이지만 ’핸드셰이크가 끝났다’는 사실이 사용자에게 보이는 지점이에요.
SYN 플러드: 3단계 구조를 노리는 공격
3-way handshake의 구조를 역이용한 공격이 SYN 플러드예요. 공격자가 SYN만 계속 보내고 마지막 ACK를 보내지 않아요. 서버는 SYN_RECEIVED 상태로 연결 반쪽을 메모리에 쌓아 두고 ACK를 기다리며, 자원이 고갈되어 정상 요청을 받지 못해요. 이에 대한 방어로 SYN 쿠키가 있어요. 서버가 SYN을 받을 때 자원을 할당하지 않고, 시퀀스 번호 자체에 암호화된 정보를 담아 돌려보내는 기법이에요. 정당한 클라이언트가 ACK로 다시 돌아올 때만 그 시퀀스를 검증하여 연결을 맺으므로, 자원 소비 없이 공격을 흘려 보내요.
어떻게 연결되는가
한 줄로 요약하면: SYN, SYN-ACK, ACK로 양방향 시퀀스 번호를 합의하고, 양쪽 모두 ESTABLISHED에 이르는 과정이에요. 3단계가 필요한 이유는 양방향 동기화이기 때문이고, 4단계가 필요 없는 이유는 세 번째 ACK가 확인과 데이터 전송을 동시에 수행하기 때문이에요. 이 양방향 시퀀스 합의가 바로 TCP 신뢰성의 출발점이며, TCP가 한 패킷 손실로 전체 연결이 멈추는 한계를 안고 있어 뒤이어 QUIC이 UDP 위에서 스트림별 신뢰성을 다시 만든 이유로 이어져요.
출처: RFC 9293: Transmission Control Protocol · 위키백과: TCP handshake · RFC 4987: SYN 쿠키와 SYN 플러드 대응
이미지: Fleshgrinder, Wikimedia Commons (Public Domain) — 원본