연결 수립과 종료
TCP가 연결 지향이라는 것은, 데이터를 보내기 전에 반드시 연결을 먼저 설정한다는 의미였습니다. 이 연결 설정 과정이 3-way handshake이고, 연결 해제 과정이 4-way handshake입니다.
면접에서 가장 빈번하게 물어보는 주제 중 하나이며, 실무에서도 연결 관련 문제를 디버깅할 때 반드시 이해해야 하는 과정입니다.
3-way Handshake
TCP 연결 수립은 세 단계로 이루어집니다.
Client Server
│ │
│ [CLOSED] [LISTEN] │
│ │
│──── SYN (seq=1000) ───────────────────────→ │
│ [SYN_SENT] │
│ │
│ ←──── SYN-ACK (seq=3000, ack=1001) ────── │
│ [SYN_RECEIVED] │
│ │
│──── ACK (ack=3001) ──────────────────────→ │
│ [ESTABLISHED] [ESTABLISHED] │
│ │
│ ← 이 시점부터 양방향 데이터 전송 가능 → │
│ │1단계 — SYN: 클라이언트가 서버에게 "연결하고 싶습니다"라는 의사를 담은 SYN(Synchronize) 패킷을 보냅니다. 이 패킷에는 클라이언트의 초기 순서 번호(ISN: Initial Sequence Number)가 포함됩니다. 예를 들어 seq=1000이라고 합시다.
2단계 — SYN-ACK: 서버가 클라이언트의 요청을 수락하며, 두 가지를 동시에 보냅니다. 클라이언트의 SYN에 대한 확인(ACK)과 서버 자신의 SYN입니다. ack=1001(클라이언트의 seq+1)과 seq=3000(서버의 ISN)을 함께 보냅니다.
3단계 — ACK: 클라이언트가 서버의 SYN에 대한 확인(ACK)을 보냅니다. ack=3001(서버의 seq+1)을 보내면 연결이 수립됩니다. 이 세 번째 패킷부터는 데이터를 함께 실어 보낼 수 있습니다.
왜 3-way일까요? 양쪽이 모두 상대방의 통신 능력을 확인해야 하기 때문입니다.
최소 확인 사항
1. Client → Server: "내가 보낸 것이 너에게 도달하는가?" (SYN)
2. Server → Client: "그래, 도달한다. 내가 보낸 것도 너에게 도달하는가?" (SYN-ACK)
3. Client → Server: "그래, 도달한다." (ACK)
2-way로는 부족한 이유
Client가 SYN을 보내고 Server가 SYN-ACK를 보내면...
Server는 자신의 SYN이 Client에 도달했는지 알 수 없음
→ Client → Server 방향은 확인됨
→ Server → Client 방향은 미확인ISN(Initial Sequence Number)의 비밀
ISN은 왜 0이나 1이 아니라 랜덤한 큰 수일까요? 보안과 신뢰성 때문입니다.
시나리오: ISN이 항상 0이라면
1차 연결: Client(seq=0) ←→ Server(seq=0)
데이터 전송 중 패킷 일부가 네트워크에 잔류
연결 종료 후 같은 IP:Port로 재연결
2차 연결: Client(seq=0) ←→ Server(seq=0)
1차 연결의 잔류 패킷이 도착 → 2차 연결의 데이터로 오인!
ISN이 랜덤이면
1차 연결: seq=157482...
2차 연결: seq=892347...
→ 잔류 패킷의 seq가 2차 연결 범위에 없으므로 폐기됨
보안 측면
ISN이 예측 가능하면 → TCP Sequence Prediction Attack
공격자가 seq를 추측하여 위조 패킷 삽입 가능
→ RFC 6528: ISN은 암호학적으로 안전한 랜덤 생성 권장4-way Handshake
TCP 연결 종료는 네 단계로 이루어집니다. 양쪽이 각각 독립적으로 연결을 종료해야 하기 때문에, 연결보다 한 단계가 더 필요합니다.
Client Server
│ │
│ [ESTABLISHED] [ESTABLISHED] │
│ │
│──── FIN (seq=5000) ──────────────────────→ │
│ [FIN_WAIT_1] │
│ │
│ ←──── ACK (ack=5001) ──────────────────── │
│ [FIN_WAIT_2] [CLOSE_WAIT] │
│ │
│ (Server가 남은 데이터 전송 가능) │
│ │
│ ←──── FIN (seq=7000) ──────────────────── │
│ [LAST_ACK] │
│ │
│──── ACK (ack=7001) ──────────────────────→ │
│ [TIME_WAIT] [CLOSED] │
│ │
│ (2MSL 대기 후) │
│ [CLOSED] │1단계 — FIN: 연결을 먼저 끊으려는 쪽(보통 클라이언트)이 FIN(Finish) 패킷을 보냅니다. "더 이상 보낼 데이터가 없습니다"라는 의미입니다.
2단계 — ACK: 상대방이 FIN을 받았다는 확인(ACK)을 보냅니다. 하지만 이 시점에서 상대방은 아직 보낼 데이터가 남아 있을 수 있습니다.
3단계 — FIN: 상대방도 보낼 데이터를 모두 전송한 후, 자신의 FIN을 보냅니다. "나도 더 이상 보낼 데이터가 없습니다."
4단계 — ACK: 처음 FIN을 보냈던 쪽이 상대방의 FIN에 대한 확인(ACK)을 보냅니다. 이 ACK가 전달되면 연결이 완전히 종료됩니다.
왜 3-way가 아니라 4-way일까요? 연결 수립 시에는 서버가 SYN과 ACK를 하나의 패킷(SYN-ACK)으로 합칠 수 있었습니다. 하지만 종료 시에는 한쪽이 FIN을 보내도 상대방은 아직 보낼 데이터가 남아 있을 수 있으므로, ACK와 FIN을 분리해야 합니다.
TCP 연결 상태 전이
TCP 연결의 전체 상태 전이를 정리합니다.
Active Open Passive Open
(Client) (Server)
│ │
▼ ▼
┌────────┐ ┌────────┐
┌────→│ CLOSED │←────────│ CLOSED │
│ └───┬────┘ └───┬────┘
│ │ SYN 전송 │ listen()
│ ▼ ▼
│ ┌──────────┐ ┌────────┐
│ │ SYN_SENT │ │ LISTEN │
│ └────┬─────┘ └───┬────┘
│ │ SYN-ACK 수신 │ SYN 수신
│ │ ACK 전송 │ SYN-ACK 전송
│ ▼ ▼
│ ┌──────────────────────────┐
│ │ ESTABLISHED │
│ └──────┬───────────┬────────┘
│ │ │
│ FIN 전송│ │FIN 수신
│ ▼ ▼
│ ┌───────────┐ ┌───────────┐
│ │FIN_WAIT_1 │ │CLOSE_WAIT │
│ └─────┬─────┘ └──────┬────┘
│ ACK수신│ FIN전송│
│ ▼ ▼
│ ┌───────────┐ ┌───────────┐
│ │FIN_WAIT_2 │ │ LAST_ACK │
│ └─────┬─────┘ └──────┬────┘
│ FIN수신│ ACK수신│
│ ▼ │
│ ┌───────────┐ │
│ │ TIME_WAIT │ │
│ └─────┬─────┘ │
│ 2MSL 타임아웃 │
└───────────┴──────────────┘TIME_WAIT 상태
연결 종료에서 가장 주의해야 할 부분이 TIME_WAIT 상태입니다.
마지막 ACK를 보낸 쪽(먼저 FIN을 보낸 쪽)은 연결을 즉시 폐기하지 않고, 일정 시간(보통 2MSL, 약 60초) 동안 TIME_WAIT 상태를 유지합니다.
이유 1: 마지막 ACK 손실 대비
Client ──── ACK ────→ Server
│
(ACK가 손실되면?)
│
Server: ACK가 안 왔네, FIN 재전송!
│
Client ←──── FIN ──── Server
│
└── TIME_WAIT 상태이므로 FIN에 대해 ACK 재전송 가능
만약 CLOSED였다면 → RST로 응답 → Server 혼란
이유 2: 지연 패킷 격리
이전 연결: 192.168.1.10:50000 ↔ 10.0.0.1:80
잔류 패킷이 네트워크 어딘가에 떠돌고 있음
TIME_WAIT 없이 즉시 같은 4-tuple로 새 연결 생성:
새 연결: 192.168.1.10:50000 ↔ 10.0.0.1:80
잔류 패킷 도착 → 새 연결의 데이터로 오인!
TIME_WAIT (60초) 동안 대기
잔류 패킷은 TTL 만료로 폐기됨
안전하게 같은 4-tuple 재사용 가능TIME_WAIT 실무 문제와 해결
고트래픽 서버에서 TIME_WAIT가 누적되면 심각한 문제가 됩니다.
#!/bin/bash
# === TIME_WAIT 모니터링 ===
echo "=== TCP 상태별 연결 수 ==="
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
echo ""
echo "=== TIME_WAIT 연결 수 ==="
ss -tan state time-wait | wc -l
echo ""
echo "=== 현재 TCP 관련 커널 파라미터 ==="
echo "tcp_tw_reuse: $(cat /proc/sys/net/ipv4/tcp_tw_reuse)"
echo "tcp_fin_timeout: $(cat /proc/sys/net/ipv4/tcp_fin_timeout)"
echo "ip_local_port_range: $(cat /proc/sys/net/ipv4/ip_local_port_range)"
echo ""
echo "=== 권장 설정 (고트래픽 서버) ==="
# TIME_WAIT 소켓 재사용 허용
# sudo sysctl -w net.ipv4.tcp_tw_reuse=1
# FIN_WAIT 타임아웃 단축 (기본 60초 → 30초)
# sudo sysctl -w net.ipv4.tcp_fin_timeout=30
# 임시 포트 범위 확대 (기본: 32768~60999)
# sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"| 해결 방법 | 설명 | 주의사항 |
|---|---|---|
| tcp_tw_reuse=1 | TIME_WAIT 소켓을 새 연결에 재사용 | timestamp 옵션 필요 |
| Connection Pool | 연결을 재사용하여 생성/종료 최소화 | DB, HTTP 클라이언트에 필수 |
| Keep-Alive | 하나의 연결로 여러 요청 처리 | HTTP/1.1 기본 동작 |
| 포트 범위 확대 | 사용 가능한 임시 포트 수 증가 | 근본 해결은 아님 |
| SO_LINGER(0) | TIME_WAIT 없이 RST로 즉시 종료 | 데이터 손실 위험, 권장하지 않음 |
비정상 종료: RST
4-way handshake는 정상적인 연결 종료 방식입니다. 하지만 비정상적인 상황에서는 RST(Reset) 패킷을 통해 연결을 즉시 끊습니다.
시나리오 1: 닫힌 포트로 연결 시도
Client ──── SYN (dst port 9999) ────→ Server
Client ←──── RST ────────────────── Server
"9999 포트에서 리슨하는 프로세스 없음"
시나리오 2: 서버 프로세스 비정상 종료
연결 중 서버 프로세스 crash
OS가 해당 연결에 RST 전송
Client ←──── RST ────────────────── Server (OS)
시나리오 3: 방화벽 차단
Client ──── SYN ────→ [Firewall] ──× ── Server
Client ←──── RST ──── [Firewall]
"방화벽이 RST로 차단 응답"
시나리오 4: 반개방 연결 감지
Server 재부팅 후, Client가 기존 연결에 데이터 전송
Server ←──── Data (seq=5000) ──── Client
Server는 이 연결을 모름 → RST 응답RST가 발생하는 대표적인 경우들입니다.
-
존재하지 않는 포트로 연결을 시도한 경우: 서버에서 해당 포트를 리슨하고 있지 않으면 RST로 응답합니다.
-
서버 프로세스가 예기치 않게 종료된 경우: 운영체제가 해당 연결에 대해 RST를 보냅니다.
-
방화벽이 연결을 차단하는 경우: 일부 방화벽은 RST로 응답하여 연결을 거부합니다.
-
애플리케이션이 소켓을 강제로 닫는 경우:
SO_LINGER옵션을 사용하여 FIN 대신 RST로 즉시 종료할 수 있습니다.
RST는 4-way handshake를 거치지 않고 연결을 즉시 폐기합니다. TIME_WAIT도 발생하지 않습니다. 빠르지만, 상대방에게 미처 전달하지 못한 데이터가 손실될 수 있으므로 신중하게 사용해야 합니다.
연결 수립/종료 실습
import socket
import time
import subprocess
import sys
def show_connection_state(description):
"""현재 TCP 연결 상태를 출력"""
print(f"\n--- {description} ---")
if sys.platform == "win32":
result = subprocess.run(
["netstat", "-an"],
capture_output=True, text=True
)
for line in result.stdout.splitlines():
if "8765" in line:
print(f" {line.strip()}")
else:
result = subprocess.run(
["ss", "-tan"],
capture_output=True, text=True
)
for line in result.stdout.splitlines():
if "8765" in line:
print(f" {line.strip()}")
# TCP 서버 시작
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 8765))
server.listen(1)
show_connection_state("서버 LISTEN 상태")
# 클라이언트 연결 (3-way handshake 발생)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8765))
conn, addr = server.accept()
show_connection_state("ESTABLISHED 상태")
# 데이터 교환
client.sendall(b"Hello TCP!")
data = conn.recv(1024)
print(f"\n수신 데이터: {data.decode()}")
# 클라이언트가 먼저 종료 (4-way handshake 시작)
client.close()
show_connection_state("클라이언트 FIN 전송 후")
time.sleep(0.1)
# 서버도 종료
conn.close()
show_connection_state("양쪽 모두 종료 후 (TIME_WAIT)")
server.close()연결 관련 면접 질문 정리
| 질문 | 핵심 답변 |
|---|---|
| 3-way handshake를 설명하세요 | SYN → SYN-ACK → ACK, 양방향 통신 능력 상호 확인 |
| 왜 2-way가 아니라 3-way인가요? | Server→Client 방향 확인에 3번째 ACK 필요 |
| 4-way handshake를 설명하세요 | FIN → ACK → FIN → ACK, 양쪽 독립적 종료 |
| 왜 3-way가 아니라 4-way인가요? | 상대방의 ACK와 FIN 사이에 잔여 데이터 전송 가능 |
| TIME_WAIT의 목적은? | 마지막 ACK 손실 대비 + 지연 패킷 격리 |
| CLOSE_WAIT이 누적되면? | 애플리케이션이 close()를 호출하지 않는 버그 |
| RST와 FIN의 차이는? | FIN은 정상 종료(데이터 완전 전송), RST는 즉시 폐기 |
| SYN Flood 공격이란? | 대량 SYN 전송 + ACK 미응답 = 서버 자원 고갈 |
다음 절에서는 TCP가 데이터를 안정적으로 전송하기 위한 핵심 메커니즘인 흐름 제어와 혼잡 제어를 살펴보겠습니다.