본문으로 건너뛰기
Previous
Next
TCP 연결 상태 완벽 가이드 | ESTABLISHED·TIME_WAIT·CLOSE_WAIT 총정리

TCP 연결 상태 완벽 가이드 | ESTABLISHED·TIME_WAIT·CLOSE_WAIT 총정리

TCP 연결 상태 완벽 가이드 | ESTABLISHED·TIME_WAIT·CLOSE_WAIT 총정리

이 글의 핵심

TCP 11가지 연결 상태(LISTEN, SYN_SENT, ESTABLISHED, FIN_WAIT, TIME_WAIT 등)의 동작 원리와 상태 전이 다이어그램. netstat으로 네트워크 디버깅하는 실전 가이드.

들어가며: TCP 상태를 이해해야 하는 이유

네트워크 애플리케이션을 개발하거나 디버깅할 때 TCP 연결 상태를 이해하는 것은 필수입니다. netstat 명령어로 본 ESTABLISHED, TIME_WAIT, CLOSE_WAIT 같은 상태들이 무엇을 의미하는지, 언제 발생하는지 알아야 성능 문제와 버그를 해결할 수 있습니다. 이 글에서 다룰 내용:

  • TCP 11가지 연결 상태
  • 3-Way Handshake와 4-Way Handshake
  • 상태 전이 다이어그램
  • netstat/ss로 상태 확인
  • 실전 디버깅 시나리오

실전 경험에서 배운 교훈

이 기술을 실무 프로젝트에 처음 도입했을 때, 공식 문서만으로는 알 수 없었던 많은 함정들이 있었습니다. 특히 프로덕션 환경에서 발생하는 엣지 케이스들은 로컬 개발 환경에서는 재현조차 되지 않았죠.

가장 어려웠던 점은 성능 최적화였습니다. 처음엔 “동작만 하면 되겠지”라고 생각했지만, 실제 사용자 트래픽이 몰리면서 병목 지점들이 하나씩 드러났습니다. 특히 데이터베이스 쿼리 최적화, 캐싱 전략, 에러 핸들링 구조 등은 여러 번의 장애를 겪으면서 개선해 나갔습니다.

이 글에서는 그런 시행착오를 통해 얻은 실전 노하우와, “이렇게 하면 안 된다”는 교훈들을 함께 정리했습니다. 특히 트러블슈팅 섹션은 실제 장애 대응 경험을 바탕으로 작성했으니, 비슷한 문제를 마주했을 때 참고하시면 도움이 될 것입니다.

1. TCP 연결 상태 개요

TCP 상태 머신

TCP는 상태 기반 프로토콜입니다. 각 소켓은 11가지 상태 중 하나에 있으며, 패킷 송수신에 따라 상태가 전이됩니다.

11가지 TCP 상태

상태설명발생 시점
CLOSED연결 없음초기 상태
LISTEN연결 대기 중서버가 포트를 열고 대기
SYN_SENT연결 요청 전송클라이언트가 SYN 전송 후
SYN_RECEIVED연결 요청 수신서버가 SYN 받고 SYN-ACK 전송
ESTABLISHED연결 수립 완료3-Way Handshake 완료
FIN_WAIT_1종료 요청 전송능동적 종료 시작 (FIN 전송)
FIN_WAIT_2종료 요청 승인 대기FIN에 대한 ACK 수신
CLOSE_WAIT종료 요청 수신상대방이 FIN 전송 (수동적 종료)
CLOSING동시 종료양쪽이 동시에 FIN 전송
LAST_ACK최종 ACK 대기CLOSE_WAIT에서 FIN 전송 후
TIME_WAIT연결 종료 대기능동적 종료 완료, 2MSL 대기

2. 연결 수립: 3-Way Handshake

왜 3-Way인가? (2-Way가 아니라)

TCP는 양방향 신뢰성 있는 통신을 제공합니다. 3-Way Handshake는 다음을 보장합니다:

  1. 클라이언트 → 서버 경로 확인: SYN
  2. 서버 → 클라이언트 경로 확인: SYN-ACK
  3. 양방향 통신 준비 완료: ACK

2-Way가 불충분한 이유:

2-Way 시나리오 (불안전):
  Client → Server: SYN
  Server → Client: SYN-ACK
  
문제:
- 서버는 SYN-ACK를 보냈지만, 클라이언트가 받았는지 알 수 없음
- 만약 SYN-ACK가 손실되면?
  → 서버는 ESTABLISHED 상태로 전환
  → 클라이언트는 여전히 SYN_SENT
  → 서버가 데이터를 보내도 클라이언트가 받지 못함
  
3-Way 해결책:
  Client → Server: SYN
  Server → Client: SYN-ACK
  Client → Server: ACK ← 이것이 핵심!
  
- ACK를 받은 서버만 ESTABLISHED로 전환
- 클라이언트도 SYN-ACK를 받았으므로 ESTABLISHED
- 양방향 통신 확실히 가능

3-Way Handshake 과정

sequenceDiagram
    participant Client
    participant Server
    
    Note over Client: CLOSED
    Note over Server: LISTEN
    
    Client->>Server: 1. SYN (seq=100)
    Note over Client: SYN_SENT
    Note right of Client: 초기 시퀀스 번호(ISN) 전송
커널이 랜덤 생성 Server->>Client: 2. SYN-ACK (seq=200, ack=101) Note over Server: SYN_RECEIVED Note left of Server: 서버의 ISN + 클라이언트 ACK
백로그 큐에 추가 Client->>Server: 3. ACK (ack=201) Note over Client: ESTABLISHED Note over Server: ESTABLISHED Note right of Client: accept() 큐로 이동
애플리케이션이 처리 가능 Note over Client,Server: 데이터 전송 가능

상태 전이 및 커널 동작

클라이언트: CLOSED → SYN_SENT → ESTABLISHED
서버:       CLOSED → LISTEN → SYN_RECEIVED → ESTABLISHED

클라이언트 측 커널 동작:

// socket() 호출: TCB (Transmission Control Block) 할당
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 상태: CLOSED
// 커널: TCB 생성, 소켓 구조체 초기화

// connect() 호출: SYN 전송
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
// 1. 초기 시퀀스 번호(ISN) 생성: 현재 시간 + 난수
//    보안: 예측 불가능한 ISN으로 SYN Flood 방어
// 2. SYN 패킷 생성: 
//    TCP 헤더: SYN 플래그 설정, seq = ISN
// 3. IP 계층으로 전달 → 네트워크로 송신
// 4. 상태: CLOSED → SYN_SENT
// 5. 재전송 타이머 시작 (보통 1초, 2초, 4초... 최대 75초)
// 6. SYN-ACK 대기 (connect()는 블로킹)

// SYN-ACK 수신 시:
// 1. ack 값 검증: ack == ISN + 1인지 확인
// 2. ACK 패킷 생성 및 전송
// 3. 상태: SYN_SENT → ESTABLISHED
// 4. connect() 반환 (블로킹 해제)

서버 측 커널 동작:

// socket() + bind() + listen() 호출
int server = socket(AF_INET, SOCK_STREAM, 0);
bind(server, (struct sockaddr*)&addr, sizeof(addr));
listen(server, 128);  // 백로그 큐 크기
// 상태: CLOSED → LISTEN
// 커널: 
//   - SYN 큐 (미완성 연결, 크기: tcp_max_syn_backlog)
//   - Accept 큐 (완성된 연결, 크기: listen backlog)

// SYN 수신 시:
// 1. SYN 큐에 공간 있는지 확인
//    - 가득 차면: SYN Cookies 사용 또는 드롭
// 2. 반-완성 연결 생성 (Half-Open Connection)
// 3. 서버의 ISN 생성
// 4. SYN-ACK 패킷 생성 및 전송:
//    seq = 서버 ISN
//    ack = 클라이언트 ISN + 1
// 5. 상태: LISTEN → SYN_RECEIVED
// 6. 재전송 타이머 시작 (SYN-ACK 재전송)

// ACK 수신 시:
// 1. SYN 큐에서 제거
// 2. Accept 큐로 이동
// 3. 상태: SYN_RECEIVED → ESTABLISHED
// 4. accept() 호출 시 Accept 큐에서 꺼냄
//    - 큐가 비어있으면 accept()는 블로킹

int client = accept(server, ...);
// accept() 반환: Accept 큐에서 ESTABLISHED 소켓 반환

백로그 큐 관리:

          [SYN 큐]              [Accept 큐]
            ↓                       ↓
┌──────────────────────┐  ┌─────────────────────┐
│ SYN_RECEIVED 연결들  │→│ ESTABLISHED 연결들   │→ accept()
│ (미완성)             │  │ (완성, 대기 중)      │
│ tcp_max_syn_backlog  │  │ listen backlog      │
└──────────────────────┘  └─────────────────────┘

SYN Flood 공격 시:
- SYN 큐가 가득 참 → 새 SYN 드롭
- 정상 클라이언트도 연결 불가 (DoS)

해결책:
1. SYN Cookies 활성화
   - SYN 큐 우회, 무상태(Stateless) 처리
   - ISN에 연결 정보 인코딩
   
2. 백로그 증가
   sudo sysctl -w net.ipv4.tcp_max_syn_backlog=8192
   sudo sysctl -w net.core.somaxconn=4096

시퀀스 번호(Sequence Number)의 중요성:

왜 랜덤 ISN을 사용하나?

고정 ISN (예: 0)의 위험:
1. 이전 연결의 지연된 패킷이 새 연결에 섞일 수 있음
2. 공격자가 ISN을 예측하여 위조 패킷 전송 가능

랜덤 ISN (RFC 6528):
  ISN = M + F(src_ip, src_port, dst_ip, dst_port, secret)
  
  M = 4 마이크로초마다 증가하는 카운터
  F = 해시 함수 (MD5 또는 SHA-1)
  secret = 부팅 시 생성된 랜덤 키
  
→ 32비트 공간을 약 4.55시간에 순환
→ 예측 불가능 (보안)
→ 이전 연결과 충돌 방지

Python으로 3-Way Handshake 관찰

import socket
import time
def client_connect():
    """클라이언트 연결 과정"""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    print("📍 상태: CLOSED")
    
    # connect() 호출 시 SYN 전송
    print("📤 SYN 전송 중...")
    print("📍 상태: SYN_SENT")
    
    sock.connect(('example.com', 80))
    
    print("✅ 연결 수립")
    print("📍 상태: ESTABLISHED")
    
    return sock
def server_listen():
    """서버 리스닝 과정"""
    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))
    
    print("📍 상태: CLOSED")
    
    server.listen(5)
    print("👂 포트 8080에서 대기 중...")
    print("📍 상태: LISTEN")
    
    client, addr = server.accept()
    print(f"✅ {addr}로부터 연결 수립")
    print("📍 상태: ESTABLISHED")
    
    return client

패킷 레벨 분석

# tcpdump로 3-Way Handshake 캡처
sudo tcpdump -i any -nn 'tcp port 80' -c 3
# 출력:
# 1. 192.168.1.100.54321 > 93.184.216.34.80: Flags [S], seq 100
# 2. 93.184.216.34.80 > 192.168.1.100.54321: Flags [S.], seq 200, ack 101
# 3. 192.168.1.100.54321 > 93.184.216.34.80: Flags [.], ack 201

3. 연결 종료: 4-Way Handshake

정상 종료 (4-Way Handshake)

sequenceDiagram
    participant Client as Client\n(능동적 종료)
    participant Server as Server\n(수동적 종료)
    
    Note over Client,Server: ESTABLISHED
    
    Client->>Server: 1. FIN (seq=300)
    Note over Client: FIN_WAIT_1
    
    Server->>Client: 2. ACK (ack=301)
    Note over Client: FIN_WAIT_2
    Note over Server: CLOSE_WAIT
    
    Note over Server: 애플리케이션이\nclose() 호출할 때까지 대기
    
    Server->>Client: 3. FIN (seq=400)
    Note over Server: LAST_ACK
    
    Client->>Server: 4. ACK (ack=401)
    Note over Client: TIME_WAIT
    Note over Server: CLOSED
    
    Note over Client: 2MSL (60초) 대기
    Note over Client: CLOSED

능동적 종료 vs 수동적 종료

# 능동적 종료 (Active Close)
# - close()를 먼저 호출하는 쪽
# - FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
client_socket.close()  # 클라이언트가 먼저 종료
# 상태: ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
# 수동적 종료 (Passive Close)
# - 상대방의 FIN을 받는 쪽
# - CLOSE_WAIT → LAST_ACK → CLOSED
# 서버는 클라이언트의 FIN을 받음
# 상태: ESTABLISHED → CLOSE_WAIT
# 애플리케이션이 close() 호출
server_socket.close()
# 상태: CLOSE_WAIT → LAST_ACK → CLOSED

4. 11가지 TCP 상태 상세 설명

1. CLOSED

초기 상태. 연결이 존재하지 않음.

2. LISTEN

서버가 특정 포트에서 연결 요청을 기다리는 상태.
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8080))
server.listen(5)  # 백로그 큐 크기 5
# 이 시점에서 상태: LISTEN
print("Server is LISTEN on port 8080")
# 확인
netstat -an | grep 8080
# tcp4  0  0  *.8080  *.*  LISTEN

3. SYN_SENT

클라이언트가 SYN 패킷을 전송하고 SYN-ACK를 기다리는 상태.
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)  # 논블로킹 모드
try:
    sock.connect(('example.com', 80))
except BlockingIOError:
    # 이 시점에서 상태: SYN_SENT
    print("Connection in progress (SYN_SENT)")
    
    # select로 연결 완료 대기
    import select
    _, writable, _ = select.select([], [sock], [], 5)
    
    if writable:
        print("Connection ESTABLISHED")

SYN_SENT가 오래 유지되는 경우:

  • 방화벽이 SYN 패킷 차단
  • 서버가 다운되어 응답 없음
  • 네트워크 지연

4. SYN_RECEIVED

서버가 SYN을 받고 SYN-ACK를 전송한 후, 최종 ACK를 기다리는 상태.

SYN Flood 공격 상세 분석:

flowchart LR
    Attacker[공격자] -->|대량 SYN| Server[서버]
    Server -->|SYN-ACK| Void[응답 없음]
    
    Note1["서버의 SYN_RECEIVED\n큐가 가득 참"]
    
    style Attacker fill:#ff6b6b
    style Void fill:#ff6b6b

SYN Flood 공격 내부 메커니즘:

일반 연결:
Client → Server: SYN
Server → Client: SYN-ACK
Client → Server: ACK ← 정상 완료
Server: SYN 큐에서 Accept 큐로 이동

SYN Flood 공격:
Attacker → Server: SYN (위조 IP: 1.1.1.1)
Server → 1.1.1.1: SYN-ACK  ← 응답 없음 (위조 IP)
Server: SYN 큐에 SYN_RECEIVED 상태로 대기

Attacker → Server: SYN (위조 IP: 2.2.2.2)
Server → 2.2.2.2: SYN-ACK  ← 응답 없음
Server: SYN 큐에 추가

... 수천 개 반복 ...

Server: SYN 큐 가득 참 (1024개)
→ 정상 클라이언트 SYN도 드롭
→ 서비스 거부 (DoS)

SYN 큐 메모리 소비:
각 SYN_RECEIVED 연결:
- TCB (Transmission Control Block): ~280 bytes
- 재전송 타이머
- 소켓 구조체

1024개 × 280 bytes = ~280KB (작지만 리소스 고갈)

재전송 타이머:
1초 → 2초 → 4초 → 8초 → 16초 → 32초
총 63초 동안 SYN 큐 점유

공격 효과:
- 초당 1000개 SYN
- 각각 63초 유지
- 필요 큐 크기: 63,000개
- 서버 마비

SYN Cookies 방어 메커니즘:

SYN Cookies 원리 (RFC 4987):

일반 방식 (상태 유지):
1. SYN 수신 → TCB 할당 → SYN 큐 저장
2. SYN-ACK 전송
3. ACK 수신 → TCB 조회 → Accept 큐 이동

문제: SYN 큐 메모리 소비 (공격에 취약)

SYN Cookies 방식 (무상태):
1. SYN 수신 → TCB 할당 안 함!
2. ISN 계산 (Stateless):
   
   ISN = hash(
     src_ip, src_port,
     dst_ip, dst_port,
     timestamp,  ← 시간 정보
     secret      ← 서버 비밀키
   ) + MSS_encoding + timestamp_encoding
   
   ISN 구조 (32 bits):
   [31:27] timestamp (5 bits, 32초 순환)
   [26:24] MSS encoding (3 bits, 8가지 MSS)
   [23:0]  hash 값 (24 bits)

3. SYN-ACK 전송 (seq=ISN)
   → SYN 큐에 저장 안 함! (메모리 절약)

4. ACK 수신 시:
   - ack 값에서 정보 복원:
     * ack - 1 = ISN
     * ISN에서 timestamp, MSS 추출
     * hash 재계산하여 검증
   
   검증:
   calculated_hash = hash(...)
   if (ack - 1) matches calculated_hash:
     → 정상 연결
     → ESTABLISHED 생성
   else:
     → 위조 패킷
     → 드롭

장점:
- SYN 큐 불필요 (메모리 무제한)
- SYN Flood 공격 무효화

단점:
- TCP 옵션 손실 (Window Scale, SACK 등)
- MSS만 8가지로 제한
- 성능 약간 저하

활성화:
Linux:
  net.ipv4.tcp_syncookies=1
  
  동작:
  - SYN 큐 < 75%: 일반 방식
  - SYN 큐 ≥ 75%: SYN Cookies 자동 활성화

확인:
$ netstat -s | grep "SYNs to LISTEN"
  123 SYNs to LISTEN sockets dropped
  456 times used SYN cookies

백로그 큐 (Backlog Queue) 상세:

listen(fd, backlog) 내부 구조:

Linux 커널 큐 2개:

1. SYN Queue (미완성 연결):
   크기: net.ipv4.tcp_max_syn_backlog (기본 1024)
   상태: SYN_RECEIVED
   
   [SYN1] [SYN2] [SYN3] ... [SYNn]

   각 항목:
   - 클라이언트 IP:Port
   - ISN
   - 재전송 타이머
   - TCP 옵션

2. Accept Queue (완성된 연결):
   크기: min(backlog, net.core.somaxconn)
   상태: ESTABLISHED
   
   [CONN1] [CONN2] [CONN3] ... [CONNn]

   accept() 호출 시 꺼냄

흐름:
SYN 수신 → SYN Queue 추가 (SYN_RECEIVED)
ACK 수신 → Accept Queue 이동 (ESTABLISHED)
accept() → Accept Queue에서 제거

큐 오버플로우:
SYN Queue 가득:
  - SYN Cookies 활성화 (tcp_syncookies=1)
  - 또는 SYN 드롭
  
Accept Queue 가득:
  - 새 ACK 무시
  - 클라이언트는 SYN-ACK 재전송 대기
  - 클라이언트 타임아웃 발생

튜닝:
# SYN 큐 증가
net.ipv4.tcp_max_syn_backlog=8192

# Accept 큐 증가
net.core.somaxconn=4096

# 애플리케이션
listen(fd, 4096)  # 큐 크기 명시

확인:
ss -lnt
State    Recv-Q  Send-Q  Local Address:Port
LISTEN   0       128     0.0.0.0:80
         ↑       ↑
    Accept큐    Accept큐 최대 크기
    현재 대기   (listen backlog)
# SYN_RECEIVED 상태 확인
netstat -an | grep SYN_RECV

# SYN Flood 방어 (Linux)
sudo sysctl -w net.ipv4.tcp_syncookies=1
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=8192

# 백로그 확인
ss -lnt

5. ESTABLISHED

연결이 수립되어 데이터를 주고받을 수 있는 상태.
# ESTABLISHED 상태에서 데이터 송수신
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('example.com', 80))
# 상태: ESTABLISHED
sock.send(b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
response = sock.recv(4096)
print(response.decode())
# ESTABLISHED 연결 수 확인
netstat -an | grep ESTABLISHED | wc -l
# 특정 포트의 ESTABLISHED 연결
netstat -an | grep ':80.*ESTABLISHED'

6. FIN_WAIT_1

능동적 종료를 시작하여 FIN을 전송한 상태. ACK를 기다림.
# 클라이언트가 먼저 종료
client_socket.close()  # FIN 전송
# 상태: ESTABLISHED → FIN_WAIT_1
# 서버의 ACK 대기

7. FIN_WAIT_2

FIN에 대한 ACK를 받았지만, 상대방의 FIN을 기다리는 상태.
sequenceDiagram
    participant C as Client
    participant S as Server
    
    Note over C,S: ESTABLISHED
    
    C->>S: FIN
    Note over C: FIN_WAIT_1
    
    S->>C: ACK
    Note over C: FIN_WAIT_2
    Note over S: CLOSE_WAIT
    
    Note over S: 서버가 close() 호출할 때까지\nFIN_WAIT_2 유지

FIN_WAIT_2 타임아웃:

# Linux에서 FIN_WAIT_2 타임아웃 설정 (기본 60초)
sudo sysctl -w net.ipv4.tcp_fin_timeout=30

8. CLOSE_WAIT - 가장 흔한 소켓 리소스 누수의 원인

상대방이 FIN을 보내 연결 종료를 요청한 상태.
애플리케이션이 close()를 호출해야 함.

CLOSE_WAIT vs TIME_WAIT 핵심 차이:

TIME_WAIT:
- 능동적 종료 측 (close()를 먼저 호출)
- 커널이 자동 관리
- 60초 후 자동 정리
- 정상적인 상태 (걱정 불필요)

CLOSE_WAIT:
- 수동적 종료 측 (FIN을 먼저 받음)
- 애플리케이션이 명시적으로 close() 호출 필요
- 애플리케이션이 close()를 안 부르면 영원히 유지
- 버그의 신호! (리소스 누수)

CLOSE_WAIT 발생 메커니즘:

# 서버 코드
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8080))
server.listen(5)

client_sock, addr = server.accept()
# 상태: ESTABLISHED

# 클라이언트가 연결을 닫음 (FIN 전송)
# 커널이 자동으로 ACK 응답
# 상태: ESTABLISHED → CLOSE_WAIT

# ⚠️ 문제: 애플리케이션이 close()를 호출하지 않음
# 상태: CLOSE_WAIT (영구 유지)
# 소켓 파일 디스크립터 누수
# 메모리 누수
# 포트 고갈 (서버 측에서는 문제 적지만)

# ✅ 해결: 반드시 close() 호출
client_sock.close()
# 상태: CLOSE_WAIT → LAST_ACK → CLOSED

CLOSE_WAIT 문제 (소켓 리소스 누수):

# ❌ 나쁜 예 1: Exception 발생 시 close() 누락
def handle_request(sock):
    data = sock.recv(1024)
    if not data:
        return  # ❌ close() 호출 안 함
    process(data)
    sock.send(b'OK')
    sock.close()  # ❌ 여기 도달하지 못할 수 있음

# ❌ 나쁜 예 2: 무한 루프에서 close() 누락
def handle_client(sock):
    while True:
        data = sock.recv(1024)
        if not data:
            break  # ❌ close() 없이 종료
        process(data)

# ❌ 나쁜 예 3: 멀티스레드에서 소켓 관리 실패
def worker(sock):
    try:
        process(sock)
    except Exception as e:
        log_error(e)
        # ❌ Exception 발생 시 close() 누락
    
# ✅ 좋은 예 1: try-finally
def handle_request(sock):
    try:
        data = sock.recv(1024)
        if not data:
            return
        process(data)
        sock.send(b'OK')
    finally:
        sock.close()  # ✅ 항상 실행됨

# ✅ 좋은 예 2: Context Manager
def handle_request(sock):
    with sock:  # ✅ __exit__에서 자동 close()
        data = sock.recv(1024)
        process(data)
        sock.send(b'OK')

# ✅ 좋은 예 3: recv(0) 감지
def handle_client(sock):
    try:
        while True:
            data = sock.recv(1024)
            if not data:  # FIN 수신 (상대방이 닫음)
                break
            process(data)
    finally:
        sock.close()  # ✅ 반드시 닫기

CLOSE_WAIT 디버깅 실전 가이드:

# 1. CLOSE_WAIT 소켓 수 확인
netstat -an | grep CLOSE_WAIT | wc -l
# 100개 이상이면 문제!

# 2. 프로세스별 소켓 수 확인
lsof -p <PID> | grep TCP | wc -l
# 또는
ls -l /proc/<PID>/fd | grep socket | wc -l

# 3. 어떤 소켓이 CLOSE_WAIT인지 확인
sudo ss -tanp | grep CLOSE_WAIT
# 출력 예시:
# CLOSE-WAIT  0   0   192.168.1.100:8080  192.168.1.200:54321  users:(("python",pid=1234,fd=5))
#                     ↑ 서버 포트           ↑ 클라이언트 포트    ↑ 프로세스 정보

# 4. 특정 프로세스의 소켓 상태
sudo lsof -p <PID> -a -i TCP
# 출력 예시:
# python 1234 user  5u  IPv4 12345  TCP *:8080 (LISTEN)
# python 1234 user  6u  IPv4 12346  TCP 192.168.1.100:8080->192.168.1.200:54321 (CLOSE_WAIT)
#                                                                                 ↑ 문제!

# 5. 파일 디스크립터 한계 확인
ulimit -n
# 1024 ← 기본값
# CLOSE_WAIT가 1024개 쌓이면 새 연결 불가!

# 6. 커널 소켓 통계
cat /proc/net/sockstat
# 출력:
# TCP: inuse 150 orphan 0 tw 50 alloc 200 mem 10
#      ↑ 사용 중   ↑ 고아       ↑ TIME_WAIT  ↑ 할당됨

실제 프로덕션 사례:

# 사례 1: Django 뷰에서 외부 API 호출
import requests

def my_view(request):
    response = requests.get('https://api.example.com/data')
    # ❌ 문제: requests는 내부적으로 소켓 사용
    # Connection: close로 응답 시 서버가 먼저 FIN 전송
    # requests가 제대로 정리 안 하면 CLOSE_WAIT 누적
    
    # ✅ 해결책 1: Session 사용 (연결 재사용)
    session = requests.Session()
    response = session.get('https://api.example.com/data')
    session.close()  # 명시적 종료
    
    # ✅ 해결책 2: Context Manager
    with requests.Session() as session:
        response = session.get('https://api.example.com/data')

# 사례 2: 데이터베이스 연결 누수
import psycopg2

def query_database():
    conn = psycopg2.connect(
        host='localhost',
        database='mydb',
        user='user',
        password='pass'
    )
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM users')
    results = cursor.fetchall()
    # ❌ conn.close() 누락!
    # PostgreSQL 서버에서 CLOSE_WAIT 누적
    
    # ✅ 해결책
    try:
        # ... 쿼리 실행 ...
    finally:
        cursor.close()
        conn.close()

# 사례 3: WebSocket 연결 관리
import websocket

def handle_websocket():
    ws = websocket.create_connection('ws://server.com/socket')
    ws.send('Hello')
    result = ws.recv()
    # ❌ ws.close() 누락!
    
    # ✅ 해결책
    try:
        ws.send('Hello')
        result = ws.recv()
    finally:
        ws.close()

CLOSE_WAIT 모니터링 자동화:

#!/bin/bash
# close_wait_monitor.sh

THRESHOLD=50
CLOSE_WAIT_COUNT=$(netstat -an | grep CLOSE_WAIT | wc -l)

if [ $CLOSE_WAIT_COUNT -gt $THRESHOLD ]; then
    echo "⚠️  ALERT: CLOSE_WAIT count is $CLOSE_WAIT_COUNT (threshold: $THRESHOLD)"
    
    # 프로세스별 통계
    echo "Top processes with CLOSE_WAIT:"
    sudo ss -tanp | grep CLOSE_WAIT | awk '{print $NF}' | sort | uniq -c | sort -rn | head -5
    
    # 슬랙 알림 전송
    curl -X POST -H 'Content-type: application/json' \
        --data "{\"text\":\"CLOSE_WAIT alert: $CLOSE_WAIT_COUNT connections\"}" \
        https://hooks.slack.com/services/YOUR/WEBHOOK/URL
fi
# Python 모니터링 스크립트
import subprocess
import time

def count_close_wait():
    result = subprocess.run(['netstat', '-an'], capture_output=True, text=True)
    count = result.stdout.count('CLOSE_WAIT')
    return count

def monitor_close_wait(threshold=50, interval=60):
    """CLOSE_WAIT 소켓을 주기적으로 모니터링"""
    while True:
        count = count_close_wait()
        print(f"CLOSE_WAIT count: {count}")
        
        if count > threshold:
            # 경고 로그
            print(f"⚠️  WARNING: CLOSE_WAIT count exceeded threshold ({count} > {threshold})")
            
            # 상세 정보 수집
            result = subprocess.run(['ss', '-tanp'], capture_output=True, text=True)
            with open('/var/log/close_wait_debug.log', 'a') as f:
                f.write(f"\n=== {time.ctime()} ===\n")
                f.write(result.stdout)
        
        time.sleep(interval)

if __name__ == '__main__':
    monitor_close_wait(threshold=50, interval=60)

CLOSE_WAIT 근본 원인 분석:

CLOSE_WAIT가 쌓이는 3가지 근본 원인:

1. 코드 버그
   - try-finally 누락
   - Exception 처리 미흡
   - 조기 return에서 close() 누락

2. 아키텍처 문제
   - 장시간 실행되는 백그라운드 작업
   - 연결을 전역 변수에 저장 후 관리 실패
   - 멀티스레딩/비동기에서 소켓 생명주기 관리 실패

3. 라이브러리 버그
   - 서드파티 라이브러리의 소켓 정리 버그
   - 오래된 버전 사용
   
해결 원칙:
→ 모든 소켓 open에 대응하는 close 보장
→ Context Manager 활용
→ 리소스 누수 테스트 자동화

9. CLOSING

양쪽이 동시에 FIN을 전송한 경우 (드물게 발생).
sequenceDiagram
    participant C as Client
    participant S as Server
    
    Note over C,S: ESTABLISHED
    
    C->>S: FIN
    Note over C: FIN_WAIT_1
    
    S->>C: FIN (동시 전송)
    Note over S: FIN_WAIT_1
    
    Note over C: CLOSING
    Note over S: CLOSING
    
    C->>S: ACK
    S->>C: ACK
    
    Note over C: TIME_WAIT
    Note over S: TIME_WAIT

10. LAST_ACK

수동적 종료에서 FIN을 전송하고 최종 ACK를 기다리는 상태.
# 서버가 수동적으로 종료
# 1. 클라이언트가 FIN 전송 → 서버는 CLOSE_WAIT
# 2. 서버가 close() 호출 → FIN 전송, LAST_ACK 상태
# 3. 클라이언트의 ACK 수신 → CLOSED

11. TIME_WAIT - TCP의 가장 오해받는 상태

연결 종료 후 2MSL(Maximum Segment Lifetime) 동안 대기.
지연된 패킷 처리 및 포트 재사용 방지.

TIME_WAIT가 반드시 필요한 2가지 이유:

1. 마지막 ACK 손실 시나리오

정상적인 4-Way Handshake:
  Client              Server
    |                   |
    |  ------ FIN ----> |
    |  <----- ACK ----- |
    |  <----- FIN ----- |
    |  ------ ACK ----> |  ← 마지막 ACK
    |                   |
  TIME_WAIT           CLOSED
  (60초 대기)

만약 마지막 ACK가 손실된다면:
  Client              Server
    |                   |
    |  ------ ACK ----> | (손실!)
    |                   |
  TIME_WAIT           LAST_ACK
                      (재전송 타이머)
    |                   |
    |  <----- FIN ----- | ← FIN 재전송
    |  ------ ACK ----> | ← 재응답
    |                   |
  TIME_WAIT           CLOSED

TIME_WAIT가 없다면?
  → 클라이언트가 이미 CLOSED
  → FIN 재전송에 RST로 응답
  → 서버가 비정상 종료로 인식
  → 애플리케이션 로그에 에러 발생

2. 지연된 패킷 문제

시나리오: 빠른 연결-종료-재연결

연결 A:
  Client:12345 ←→ Server:80
  - 데이터 전송 중 일부 패킷이 네트워크에서 지연
  - 연결 종료
  
즉시 재연결 (TIME_WAIT 없이):
  Client:12345 ←→ Server:80 (동일한 4-튜플!)
  
문제:
  - 이전 연결 A의 지연된 패킷이 도착
  - 커널이 새 연결 B의 패킷으로 오인
  - 데이터 손상 발생!

TIME_WAIT 해결책:
  - 60초 동안 같은 4-튜플 사용 금지
  - MSL(보통 30초) * 2 = 60초
  - 네트워크의 모든 패킷이 소멸될 때까지 대기
  - 60초 후에만 포트 재사용 가능

2MSL (Maximum Segment Lifetime) 계산:

MSL = 네트워크에서 패킷이 살 수 있는 최대 시간
     = IP TTL * 홉 시간
     = 보통 30초 (RFC 1122 권장)

2MSL = 60초
  = 송신 패킷 소멸 시간 (30초)
  + 응답 패킷 소멸 시간 (30초)

Linux 기본값:
  net.ipv4.tcp_fin_timeout = 60 (초)
  
조정 가능하지만 권장하지 않음:
  sudo sysctl -w net.ipv4.tcp_fin_timeout=30
  # 위험: 지연된 패킷 문제 발생 가능

TIME_WAIT 문제 (포트 고갈)

# 문제 재현 코드
import socket
import time

# 클라이언트가 짧은 연결을 반복 생성
for i in range(65000):  # 포트 범위 초과 시도
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(('server.com', 80))
    sock.send(b'GET / HTTP/1.1\r\n\r\n')
    sock.recv(1024)
    sock.close()  # 능동적 종료 → TIME_WAIT
    # 이 소켓은 60초 동안 포트 점유
    print(f"Connection {i} closed")

# 에러 발생:
# OSError: [Errno 99] Cannot assign requested address
# 원인: 사용 가능한 포트(32768~60999) 모두 TIME_WAIT 상태
# TIME_WAIT 소켓 수 확인
netstat -an | grep TIME_WAIT | wc -l

# 많은 경우 (수천 개):
# - 클라이언트가 짧은 연결을 반복적으로 생성
# - 로드 밸런서, 프록시에서 흔함
# - HTTP/1.0 (Connection: close)

# 포트 범위 확인
cat /proc/sys/net/ipv4/ip_local_port_range
# 32768  60999
# → 약 28,000개 포트만 사용 가능

TIME_WAIT 최적화 전략:

# 1. SO_REUSEADDR (서버 측)
# - TIME_WAIT 소켓이 있어도 바인딩 허용
# - 서버 재시작 시 "Address already in use" 방지
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))
# 이전 서버의 TIME_WAIT 소켓이 있어도 바인딩 성공
# 2. SO_LINGER (위험! 신중히 사용)
import socket
import struct

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# linger = (onoff, linger_time)
# onoff=1, linger_time=0: close() 시 즉시 RST 전송 (TIME_WAIT 건너뜀)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
sock.connect(('server.com', 80))
sock.send(b'data')
sock.close()  # RST 전송, TIME_WAIT 없음

# ⚠️ 위험:
# - 데이터 손실 가능 (송신 버퍼에 남은 데이터 버림)
# - 서버가 "Connection reset by peer" 에러 발생
# - 오직 로컬 테스트용으로만 사용
# 3. net.ipv4.tcp_tw_reuse (클라이언트 측)
# - 새로운 아웃바운드 연결 시 TIME_WAIT 소켓 재사용
sudo sysctl -w net.ipv4.tcp_tw_reuse=1

# 안전한 이유:
# - 타임스탬프 옵션으로 이전 패킷 구분
# - 클라이언트에서만 작동 (서버는 영향 없음)
# 4. net.ipv4.tcp_tw_recycle (금지!)
# - Linux 4.12에서 제거됨
# - NAT 환경에서 심각한 문제 발생
sudo sysctl -w net.ipv4.tcp_tw_recycle=1  # ❌ 사용 금지!

# 문제:
# - 같은 NAT 뒤의 여러 클라이언트가 같은 IP로 보임
# - 타임스탬프 역전 시 패킷 드롭
# - 무작위 연결 실패

TIME_WAIT 근본 해결책: Keep-Alive

# ❌ 나쁜 예: 매번 새 연결 (TIME_WAIT 누적)
for i in range(1000):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(('server.com', 80))
    sock.send(b'GET / HTTP/1.1\r\nHost: server.com\r\n\r\n')
    sock.recv(1024)
    sock.close()  # TIME_WAIT

# ✅ 좋은 예: 연결 재사용 (HTTP/1.1 Keep-Alive)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('server.com', 80))
for i in range(1000):
    sock.send(b'GET / HTTP/1.1\r\nHost: server.com\r\nConnection: keep-alive\r\n\r\n')
    sock.recv(1024)
# 한 번만 close() → TIME_WAIT 1개만
sock.close()
# HTTP/2, HTTP/3: 멀티플렉싱
# - 하나의 TCP 연결로 여러 요청 처리
# - TIME_WAIT 문제 거의 없음

언제 TIME_WAIT가 문제가 되나?

문제 되는 경우:
1. 로드 밸런서 / 리버스 프록시
   - 백엔드 서버로 짧은 연결 반복 생성
   - 포트 고갈 위험

2. 크롤러 / 스크래퍼
   - 수천 개 사이트에 연결
   - 빠른 속도로 연결 생성/종료

3. 부하 테스트 도구
   - 초당 수만 개 요청
   - 클라이언트 포트 고갈

문제 안 되는 경우:
1. 일반 웹 서버 (서버는 TIME_WAIT 거의 없음)
   - 클라이언트가 능동 종료
   - 서버는 CLOSE_WAIT → CLOSED

2. Keep-Alive 사용하는 애플리케이션
   - 연결 재사용
   - TIME_WAIT 최소화

3. 서버 측 (listen 소켓)
   - LISTEN 소켓은 TIME_WAIT 없음
   - Accept된 소켓만 영향받음

5. 상태 전이 다이어그램

완전한 TCP 상태 전이

stateDiagram-v2
    [*] --> CLOSED
    
    CLOSED --> LISTEN: passive open\n(server)
    CLOSED --> SYN_SENT: active open\n(client)
    
    LISTEN --> SYN_RECEIVED: recv SYN\nsend SYN-ACK
    
    SYN_SENT --> ESTABLISHED: recv SYN-ACK\nsend ACK
    SYN_SENT --> SYN_RECEIVED: recv SYN\nsend SYN-ACK
    
    SYN_RECEIVED --> ESTABLISHED: recv ACK
    
    ESTABLISHED --> FIN_WAIT_1: close()\nsend FIN
    ESTABLISHED --> CLOSE_WAIT: recv FIN\nsend ACK
    
    FIN_WAIT_1 --> FIN_WAIT_2: recv ACK
    FIN_WAIT_1 --> CLOSING: recv FIN\nsend ACK
    
    FIN_WAIT_2 --> TIME_WAIT: recv FIN\nsend ACK
    
    CLOSE_WAIT --> LAST_ACK: close()\nsend FIN
    
    CLOSING --> TIME_WAIT: recv ACK
    
    LAST_ACK --> CLOSED: recv ACK
    
    TIME_WAIT --> CLOSED: 2MSL timeout
    
    CLOSED --> [*]

클라이언트 vs 서버 상태 흐름

flowchart TB
    subgraph Client["클라이언트 (능동적 종료)"]
        C1[CLOSED] --> C2[SYN_SENT]
        C2 --> C3[ESTABLISHED]
        C3 --> C4[FIN_WAIT_1]
        C4 --> C5[FIN_WAIT_2]
        C5 --> C6[TIME_WAIT]
        C6 --> C7[CLOSED]
    end
    
    subgraph Server["서버 (수동적 종료)"]
        S1[CLOSED] --> S2[LISTEN]
        S2 --> S3[SYN_RECEIVED]
        S3 --> S4[ESTABLISHED]
        S4 --> S5[CLOSE_WAIT]
        S5 --> S6[LAST_ACK]
        S6 --> S7[CLOSED]
    end

6. netstat/ss로 상태 확인

netstat 명령어

기본 사용법

# 모든 TCP 연결 확인
netstat -an | grep tcp
# 특정 상태만 필터링
netstat -an | grep ESTABLISHED
netstat -an | grep TIME_WAIT
netstat -an | grep CLOSE_WAIT
# 프로세스 정보 포함 (Linux)
sudo netstat -anp | grep :80
# 통계 정보
netstat -s | grep -i tcp

상태별 개수 확인

# Linux/Mac
netstat -an | awk '/tcp/ {print $6}' | sort | uniq -c
# 출력 예시:
#   150 ESTABLISHED
#    50 TIME_WAIT
#     5 CLOSE_WAIT
#     3 LISTEN

ss 명령어 (더 빠름)

# 모든 TCP 소켓
ss -tan
# ESTABLISHED 상태만
ss -tan state established
# TIME_WAIT 상태만
ss -tan state time-wait
# 프로세스 정보 포함
ss -tanp
# 통계
ss -s

ss 필터 예시

# 특정 포트
ss -tan '( dport = :80 or sport = :80 )'
# 특정 IP
ss -tan dst 192.168.1.100
# 여러 상태 조합
ss -tan state established state syn-sent
# 수신 큐와 송신 큐 크기 확인
ss -tan | awk '{print $2, $3}'

Windows PowerShell

# TCP 연결 확인
Get-NetTCPConnection
# ESTABLISHED 상태만
Get-NetTCPConnection -State Established
# 특정 포트
Get-NetTCPConnection -LocalPort 80
# 상태별 개수
Get-NetTCPConnection | Group-Object -Property State | Select-Object Name, Count

7. 실전 디버깅 시나리오

시나리오 1: TIME_WAIT 과다 (포트 고갈)

문제 증상

$ netstat -an | grep TIME_WAIT | wc -l
5000
# 새 연결 시도 시 에러
# "Cannot assign requested address"

원인

# ❌ 잘못된 코드: 짧은 연결 반복 생성
for i in range(10000):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(('api.example.com', 80))
    sock.send(b'GET / HTTP/1.1\r\n\r\n')
    sock.recv(1024)
    sock.close()  # TIME_WAIT 상태로 전환
    # 60초 동안 포트 재사용 불가!

해결 방법

# ✅ 해결 1: 연결 재사용 (Keep-Alive)
import requests
session = requests.Session()
for i in range(10000):
    response = session.get('http://api.example.com/')
    # 같은 연결 재사용, TIME_WAIT 발생 안 함
# ✅ 해결 2: SO_REUSEADDR 설정
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# ✅ 해결 3: 커널 파라미터 조정 (Linux)
# /etc/sysctl.conf
# net.ipv4.tcp_tw_reuse = 1
# net.ipv4.tcp_fin_timeout = 30

시나리오 2: CLOSE_WAIT 누적 (리소스 누수)

문제 증상

$ netstat -an | grep CLOSE_WAIT | wc -l
1000
# 시간이 지나도 줄어들지 않음
# 결국 "Too many open files" 에러

원인

# ❌ 잘못된 코드: 소켓을 닫지 않음
def handle_client(sock):
    data = sock.recv(1024)
    process(data)
    # sock.close() 누락!
    # 클라이언트가 연결을 닫으면 CLOSE_WAIT 상태로 남음
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8080))
server.listen(5)
while True:
    client, addr = server.accept()
    handle_client(client)  # 소켓 누수!

해결 방법

# ✅ 해결 1: try-finally로 확실히 닫기
def handle_client(sock):
    try:
        data = sock.recv(1024)
        process(data)
    finally:
        sock.close()  # 반드시 실행
# ✅ 해결 2: Context Manager 사용
def handle_client(sock):
    with sock:
        data = sock.recv(1024)
        process(data)
    # 자동으로 close() 호출
# ✅ 해결 3: 타임아웃 설정
sock.settimeout(30)  # 30초 후 자동 종료

디버깅

# CLOSE_WAIT 소켓을 가진 프로세스 찾기
lsof -i -n | grep CLOSE_WAIT
# 프로세스별 CLOSE_WAIT 개수
lsof -i -n | grep CLOSE_WAIT | awk '{print $2}' | sort | uniq -c
# 특정 프로세스의 소켓 상태
lsof -p <PID> | grep TCP

시나리오 3: SYN_SENT 타임아웃

문제 증상

import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
try:
    sock.connect(('unreachable-server.com', 80))
except socket.timeout:
    print("❌ Connection timeout (SYN_SENT)")

원인 및 해결

# 1. 방화벽 확인
sudo iptables -L -n | grep 80
# 2. 라우팅 확인
traceroute unreachable-server.com
# 3. 서버 상태 확인
ping unreachable-server.com
# 4. 포트 스캔
nmap -p 80 unreachable-server.com

시나리오 4: 대량 ESTABLISHED 연결

문제 증상

$ netstat -an | grep ESTABLISHED | wc -l
10000
# 서버 응답 느려짐
# CPU, 메모리 사용량 증가

원인

# ❌ Keep-Alive로 연결을 무한정 유지
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# 유휴 연결이 계속 ESTABLISHED 상태로 남음

해결 방법

# ✅ 해결 1: 타임아웃 설정
sock.settimeout(60)  # 60초 유휴 시 자동 종료
# ✅ 해결 2: Keep-Alive 파라미터 조정
import socket
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Linux
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)    # 60초 후 첫 probe
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)   # 10초 간격
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)      # 3번 시도
# ✅ 해결 3: 연결 풀 크기 제한
from concurrent.futures import ThreadPoolExecutor
max_connections = 1000
executor = ThreadPoolExecutor(max_workers=max_connections)

8. 성능 튜닝

Port Exhaustion (포트 고갈) 문제

클라이언트 포트 고갈 메커니즘:

TCP 연결 4-Tuple:
(Client IP, Client Port, Server IP, Server Port)

서버:
- 고정 포트 (예: 80, 443)
- 여러 클라이언트 IP 수용

클라이언트:
- 동적 포트 (Ephemeral Port)
- 범위: 32768 ~ 60999 (Linux)
- 총 개수: 28232개

예시: 클라이언트가 API 서버에 대량 요청

for i in range(100000):
    sock = requests.get('https://api.example.com')
    sock.close()

연결 과정:
1. 새 연결 생성:
   Client (192.168.1.100:32768) → Server (1.2.3.4:443)
   
2. 요청 완료, close() 호출:
   FIN 전송 → TIME_WAIT (60초)
   
3. 다음 연결:
   Client (192.168.1.100:32769) → Server (1.2.3.4:443)
   
4. 포트 증가...
   32770, 32771, ... 60999
   
5. 포트 고갈:
   28232개 모두 TIME_WAIT 상태
   → 새 연결 불가!

증상:
socket.error: [Errno 99] Cannot assign requested address
또는
socket.error: [Errno 24] Too many open files

TIME_WAIT 소켓 확인:
$ netstat -an | grep TIME_WAIT | wc -l
25000  ← 포트 거의 고갈

4-Tuple 유니크 제약:
(Client IP, Client Port, Server IP, Server Port)

같은 서버 (1.2.3.4:443)에 연결 시:
- Client IP: 고정 (192.168.1.100)
- Client Port: 28232개로 제한
- Server IP:Port: 고정 (1.2.3.4:443)
→ 최대 28232개 동시 연결

해결책:
1. TIME_WAIT 재사용 활성화
2. 포트 범위 확장
3. 연결 풀 사용
4. HTTP Keep-Alive

해결 방법 상세:

1. TIME_WAIT 재사용 (net.ipv4.tcp_tw_reuse):

기본 동작:
- TIME_WAIT 소켓은 60초간 포트 점유
- 새 연결은 사용 불가

tcp_tw_reuse=1:
- 타임스탬프 기반 재사용 허용
- 조건:
  * 새 연결의 타임스탬프 > 이전 연결
  * 클라이언트 측에서만 안전
  * 서버에서는 사용 금지 (들어오는 연결 혼란)

설정:
sudo sysctl -w net.ipv4.tcp_tw_reuse=1

효과:
- TIME_WAIT 소켓을 1초 후 재사용 가능
- 포트 고갈 문제 해결
- 클라이언트 벤치마크/크롤러에 유용

주의:
- NAT 환경에서 타임스탬프 꼬일 수 있음
- 방화벽이 타임스탬프 제거하면 효과 없음

2. 포트 범위 확장:

기본:
net.ipv4.ip_local_port_range = 32768 60999
총 28232개

확장:
sudo sysctl -w net.ipv4.ip_local_port_range="10000 65535"
총 55536개 (거의 2배)

효과:
- 동시 연결 수 증가
- TIME_WAIT 대기 시간 동안 더 많은 포트 사용 가능

3. SO_LINGER 옵션 (주의 필요):

sock.setsockopt(
  socket.SOL_SOCKET, 
  socket.SO_LINGER, 
  struct.pack('ii', 1, 0)
)

효과:
- close() 시 FIN 대신 RST 전송
- TIME_WAIT 우회 → 즉시 포트 재사용

부작용:
- 비정상 종료로 간주
- 버퍼에 남은 데이터 손실
- 상대방 "Connection reset by peer" 에러
→ 실무에서 비권장

4. 연결 풀 (Connection Pool):

문제:
for i in range(10000):
    r = requests.get(url)  # 매번 새 연결
    
TIME_WAIT 대량 발생

해결:
session = requests.Session()  # 연결 풀 생성
for i in range(10000):
    r = session.get(url)  # 연결 재사용
    
HTTP Keep-Alive:
Connection: keep-alive
→ 같은 소켓으로 여러 요청
→ TIME_WAIT 최소화

예시:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()
adapter = HTTPAdapter(
    pool_connections=10,    # 호스트당 연결 풀 크기
    pool_maxsize=20,        # 최대 연결 수
    max_retries=Retry(total=3)
)
session.mount('https://', adapter)

for i in range(10000):
    r = session.get('https://api.example.com')
    
결과:
- 연결 재사용
- TIME_WAIT 10~20개로 제한
- 포트 고갈 문제 해결

5. 여러 IP 사용 (Multi-Homing):

4-Tuple 제약:
(Client IP, Client Port, Server IP, Server Port)

Client IP 추가:
eth0: 192.168.1.100  → 28232개 포트
eth0:0: 192.168.1.101  → 28232개 포트
eth0:1: 192.168.1.102  → 28232개 포트

총: 84696개 동시 연결 가능

코드:
sock = socket.socket()
sock.bind(('192.168.1.101', 0))  # 특정 IP 바인딩
sock.connect(('1.2.3.4', 443))

6. 서버 측 대응 (역방향 문제):

클라이언트가 서버를 공격:
- 대량 연결 생성 후 즉시 종료
- 서버에 TIME_WAIT 대량 발생
- 서버 포트는 고정이므로 직접 영향 없음
- 하지만 메모리/소켓 디스크립터 고갈

해결:
- 클라이언트 IP당 연결 수 제한
- Rate Limiting
- DDoS 방어 시스템

포트 고갈 모니터링:

# TIME_WAIT 소켓 수 확인
netstat -an | grep TIME_WAIT | wc -l

# 특정 서버로의 TIME_WAIT
netstat -an | grep '1.2.3.4:443' | grep TIME_WAIT | wc -l

# 포트 범위 확인
cat /proc/sys/net/ipv4/ip_local_port_range

# 현재 사용 중인 포트
ss -tan | awk '{print $4}' | grep -o ':[0-9]*$' | sort | uniq | wc -l

# 실시간 모니터링
watch -n 1 'ss -tan state time-wait | wc -l'

# 포트별 상태 분포
ss -tan | awk '{print $1}' | sort | uniq -c

Linux 커널 파라미터

# /etc/sysctl.conf

# TIME_WAIT 재사용 허용 (클라이언트 측)
net.ipv4.tcp_tw_reuse = 1

# TIME_WAIT 재활용 (주의: NAT 환경에서 문제, 비활성화 권장)
net.ipv4.tcp_tw_recycle = 0

# 포트 범위 확장 (클라이언트)
net.ipv4.ip_local_port_range = 10000 65535

# FIN_WAIT_2 타임아웃 (기본 60초)
net.ipv4.tcp_fin_timeout = 30

# SYN 백로그 큐 크기
net.ipv4.tcp_max_syn_backlog = 8192

# 최대 연결 수 (서버 listen backlog)
net.core.somaxconn = 65535

# Keep-Alive 설정
net.ipv4.tcp_keepalive_time = 60     # 60초 후 첫 probe
net.ipv4.tcp_keepalive_intvl = 10    # 10초 간격
net.ipv4.tcp_keepalive_probes = 3    # 3번 시도

# 최대 파일 디스크립터 수
fs.file-max = 2097152

# 적용
sudo sysctl -p

SO_LINGER 옵션

import socket
import struct
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# SO_LINGER 설정
# l_onoff=1, l_linger=0: close() 시 RST 전송 (TIME_WAIT 회피)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
sock.connect(('example.com', 80))
sock.send(b'data')
# close() 호출 시:
# - 정상: FIN 전송 → TIME_WAIT
# - SO_LINGER(0): RST 전송 → 즉시 CLOSED
sock.close()
# ⚠️ 주의: RST는 비정상 종료로 간주됨
# 상대방이 "Connection reset by peer" 에러 발생 가능

연결 풀 패턴

import queue
import socket
import threading
class ConnectionPool:
    def __init__(self, host, port, pool_size=10):
        self.host = host
        self.port = port
        self.pool = queue.Queue(maxsize=pool_size)
        
        # 미리 연결 생성
        for _ in range(pool_size):
            sock = self._create_connection()
            self.pool.put(sock)
    
    def _create_connection(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((self.host, self.port))
        return sock
    
    def get_connection(self, timeout=5):
        """연결 가져오기"""
        try:
            return self.pool.get(timeout=timeout)
        except queue.Empty:
            return self._create_connection()
    
    def return_connection(self, sock):
        """연결 반환"""
        try:
            self.pool.put_nowait(sock)
        except queue.Full:
            sock.close()
    
    def close_all(self):
        """모든 연결 종료"""
        while not self.pool.empty():
            try:
                sock = self.pool.get_nowait()
                sock.close()
            except queue.Empty:
                break
# 사용
pool = ConnectionPool('api.example.com', 80, pool_size=20)
def make_request():
    sock = pool.get_connection()
    try:
        sock.send(b'GET / HTTP/1.1\r\n\r\n')
        response = sock.recv(4096)
        return response
    finally:
        pool.return_connection(sock)  # 재사용
# 여러 스레드에서 사용
for _ in range(1000):
    threading.Thread(target=make_request).start()
# TIME_WAIT 발생 안 함 (연결 재사용)

고급 주제

TCP Half-Open 연결

# Half-Open: 한쪽은 ESTABLISHED, 다른 쪽은 CLOSED
# 발생 원인: 네트워크 단절, 프로세스 강제 종료
# 감지 방법: Keep-Alive
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Keep-Alive probe 전송 → 응답 없으면 연결 종료

TCP Reset (RST)

# RST 패킷이 전송되는 경우:
# 1. 존재하지 않는 포트로 연결 시도
# 2. SO_LINGER(0)로 close() 호출
# 3. 애플리케이션 강제 종료
# 4. 방화벽이 연결 차단
# RST 수신 시:
# - 즉시 CLOSED 상태로 전환
# - "Connection reset by peer" 에러

TCP Simultaneous Open

sequenceDiagram
    participant A as Host A
    participant B as Host B
    
    Note over A: CLOSED
    Note over B: CLOSED
    
    A->>B: SYN
    Note over A: SYN_SENT
    
    B->>A: SYN (동시 전송)
    Note over B: SYN_SENT
    
    Note over A: SYN_RECEIVED
    Note over B: SYN_RECEIVED
    
    A->>B: SYN-ACK
    B->>A: SYN-ACK
    
    Note over A: ESTABLISHED
    Note over B: ESTABLISHED

실전 모니터링 스크립트

Python 모니터링 도구

#!/usr/bin/env python3
import subprocess
import time
from collections import Counter
def get_tcp_states():
    """TCP 상태별 개수 조회"""
    try:
        # ss 명령어 사용 (더 빠름)
        result = subprocess.run(
            ['ss', '-tan'],
            capture_output=True,
            text=True
        )
        
        lines = result.stdout.strip().split('\n')[1:]  # 헤더 제외
        states = []
        
        for line in lines:
            parts = line.split()
            if len(parts) > 0:
                state = parts[0]
                states.append(state)
        
        return Counter(states)
    
    except FileNotFoundError:
        # ss가 없으면 netstat 사용
        result = subprocess.run(
            ['netstat', '-an'],
            capture_output=True,
            text=True
        )
        
        states = []
        for line in result.stdout.split('\n'):
            if 'tcp' in line.lower():
                parts = line.split()
                if len(parts) >= 6:
                    states.append(parts[5])
        
        return Counter(states)
def monitor_tcp_states(interval=5):
    """TCP 상태 실시간 모니터링"""
    print("🔍 TCP 연결 상태 모니터링 시작...\n")
    
    try:
        while True:
            states = get_tcp_states()
            
            print(f"\n{'='*50}")
            print(f"⏰ {time.strftime('%Y-%m-%d %H:%M:%S')}")
            print(f"{'='*50}")
            
            for state, count in sorted(states.items()):
                bar = '█' * min(count, 50)
                print(f"{state:15s} {count:5d} {bar}")
            
            total = sum(states.values())
            print(f"{'─'*50}")
            print(f"{'TOTAL':15s} {total:5d}")
            
            # 경고
            if states.get('CLOSE_WAIT', 0) > 100:
                print("\n⚠️  WARNING: Too many CLOSE_WAIT connections!")
                print("   → Check if application is closing sockets properly")
            
            if states.get('TIME_WAIT', 0) > 5000:
                print("\n⚠️  WARNING: Too many TIME_WAIT connections!")
                print("   → Consider using connection pooling")
            
            time.sleep(interval)
    
    except KeyboardInterrupt:
        print("\n\n✅ 모니터링 종료")
if __name__ == '__main__':
    monitor_tcp_states(interval=5)

Bash 원라이너 모음

# 1. 상태별 개수
netstat -an | awk '/tcp/ {print $6}' | sort | uniq -c | sort -rn
# 2. 특정 포트의 연결 수
netstat -an | grep ':80 ' | wc -l
# 3. IP별 연결 수 (상위 10개)
netstat -an | grep ESTABLISHED | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -10
# 4. TIME_WAIT 비율
echo "scale=2; $(netstat -an | grep TIME_WAIT | wc -l) / $(netstat -an | grep tcp | wc -l) * 100" | bc
# 5. 연결 수 실시간 모니터링
watch -n 1 'netstat -an | grep tcp | awk "{print \$6}" | sort | uniq -c'

프로그래밍 언어별 상태 확인

Python

import socket
import psutil
# 현재 프로세스의 연결 상태
connections = psutil.net_connections(kind='tcp')
for conn in connections:
    if conn.status == 'ESTABLISHED':
        print(f"{conn.laddr.ip}:{conn.laddr.port} -> {conn.raddr.ip}:{conn.raddr.port}")
        print(f"  Status: {conn.status}")
        print(f"  PID: {conn.pid}")
# 상태별 개수
from collections import Counter
states = Counter(conn.status for conn in connections)
print(states)

Node.js

const { exec } = require('child_process');
function getTCPStates(callback) {
  exec('netstat -an | grep tcp', (error, stdout) => {
    if (error) {
      callback(error);
      return;
    }
    
    const lines = stdout.trim().split('\n');
    const states = {};
    
    lines.forEach(line => {
      const parts = line.split(/\s+/);
      const state = parts[5];
      states[state] = (states[state] || 0) + 1;
    });
    
    callback(null, states);
  });
}
// 사용
getTCPStates((err, states) => {
  if (!err) {
    console.log('TCP States:', states);
  }
});

Go

package main
import (
    "fmt"
    "github.com/shirou/gopsutil/v3/net"
)
func main() {
    // TCP 연결 조회
    connections, err := net.Connections("tcp")
    if err != nil {
        panic(err)
    }
    
    // 상태별 개수
    states := make(map[string]int)
    for _, conn := range connections {
        states[conn.Status]++
    }
    
    // 출력
    for state, count := range states {
        fmt.Printf("%s: %d\n", state, count)
    }
}

실전 팁

1. 로드 밸런서 설정

# Nginx에서 Keep-Alive 설정
upstream backend {
    server backend1.example.com:8080;
    server backend2.example.com:8080;
    
    # Keep-Alive 연결 유지
    keepalive 32;
}
server {
    location / {
        proxy_pass http://backend;
        
        # Keep-Alive 헤더
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        
        # 타임아웃
        proxy_connect_timeout 5s;
        proxy_read_timeout 60s;
    }
}

2. 데이터베이스 연결 풀

import psycopg2
from psycopg2 import pool
# 연결 풀 생성
connection_pool = psycopg2.pool.ThreadedConnectionPool(
    minconn=5,      # 최소 연결 수
    maxconn=20,     # 최대 연결 수
    host='localhost',
    database='mydb',
    user='user',
    password='pass'
)
def query_database(sql):
    conn = connection_pool.getconn()
    try:
        cursor = conn.cursor()
        cursor.execute(sql)
        result = cursor.fetchall()
        return result
    finally:
        connection_pool.putconn(conn)  # 연결 반환
# TIME_WAIT 발생 안 함 (연결 재사용)

3. HTTP 클라이언트 최적화

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
# 세션 재사용 + 재시도 전략
session = requests.Session()
retry_strategy = Retry(
    total=3,
    backoff_factor=1,
    status_forcelist=[429, 500, 502, 503, 504]
)
adapter = HTTPAdapter(
    max_retries=retry_strategy,
    pool_connections=10,  # 연결 풀 크기
    pool_maxsize=20
)
session.mount("http://", adapter)
session.mount("https://", adapter)
# 사용
for i in range(1000):
    response = session.get('http://api.example.com/data')
    # 연결 재사용, TIME_WAIT 최소화

문제 해결 체크리스트

TIME_WAIT 과다

  • HTTP Keep-Alive 사용 확인
  • 연결 풀 구현 확인
  • net.ipv4.tcp_tw_reuse 활성화
  • net.ipv4.tcp_fin_timeout 감소 (30초)
  • 클라이언트 포트 범위 확대
# 클라이언트 포트 범위 확대
sudo sysctl -w net.ipv4.ip_local_port_range="10000 65535"

CLOSE_WAIT 누적

  • 애플리케이션 코드에서 close() 호출 확인
  • try-finally 또는 with 문 사용
  • 타임아웃 설정 확인
  • 예외 처리 확인
  • 프로세스 재시작 (임시 해결)
# CLOSE_WAIT 디버깅
import traceback
def handle_connection(sock):
    try:
        data = sock.recv(1024)
        process(data)
    except Exception as e:
        print(f"❌ Error: {e}")
        traceback.print_exc()
    finally:
        print("🔒 Closing socket")
        sock.close()  # 반드시 실행

ESTABLISHED 과다

  • 유휴 연결 타임아웃 설정
  • Keep-Alive 파라미터 조정
  • 최대 연결 수 제한
  • 연결 풀 크기 조정
  • 로드 밸런서 설정 확인

정리

TCP 상태 요약

flowchart TD
    Start[연결 시작] --> Handshake[3-Way Handshake]
    Handshake --> Est["ESTABLISHED\n데이터 전송"]
    Est --> Close[연결 종료]
    Close --> Active{누가 먼저\n종료?}
    
    Active -->|클라이언트| TimeWait["TIME_WAIT\n60초 대기"]
    Active -->|서버| CloseWait["CLOSE_WAIT\nclose 호출 필요"]
    
    TimeWait --> End[CLOSED]
    CloseWait --> LastAck[LAST_ACK]
    LastAck --> End

핵심 포인트

상태주의사항해결 방법
TIME_WAIT포트 고갈 가능연결 재사용, Keep-Alive
CLOSE_WAIT소켓 누수반드시 close() 호출
SYN_SENT연결 타임아웃타임아웃 설정, 재시도
ESTABLISHED과다 연결연결 풀, 타임아웃

디버깅 명령어 요약

# 상태 확인
netstat -an | grep tcp
ss -tan
# 상태별 개수
netstat -an | awk '/tcp/ {print $6}' | sort | uniq -c
# 프로세스별 연결
lsof -i -n -P | grep TCP
# 실시간 모니터링
watch -n 1 'ss -s'
# 특정 포트
netstat -an | grep ':80'
ss -tan '( sport = :80 or dport = :80 )'

성능 최적화 요약

# ✅ 연결 재사용
session = requests.Session()
# ✅ 연결 풀
pool = ConnectionPool(size=20)
# ✅ 타임아웃 설정
sock.settimeout(30)
# ✅ Keep-Alive
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# ✅ 리소스 정리
try:
    # 작업
    pass
finally:
    sock.close()

관련 글 (내부 링크)

TCP와 함께 보면 좋은 네트워크 관련 가이드입니다:



자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. TCP 11가지 연결 상태(LISTEN, SYN_SENT, ESTABLISHED, FIN_WAIT, TIME_WAIT 등)의 동작 원리와 상태 전이 다이어그램. netstat으로 네트워크 디버깅하는 실전 가이드. S… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

참고 자료

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「TCP 연결 상태 완벽 가이드 | ESTABLISHED·TIME_WAIT·CLOSE_WAIT 총정리」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「TCP 연결 상태 완벽 가이드 | ESTABLISHED·TIME_WAIT·CLOSE_WAIT 총정리」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

TCP, 네트워크, netstat, ESTABLISHED, TIME_WAIT, CLOSE_WAIT, 소켓프로그래밍 등으로 검색하시면 이 글이 도움이 됩니다.