TIME_WAIT란 무엇이고 왜 발생하는가?

송현진·2025년 6월 1일
0

Network

목록 보기
1/6

TIME_WAIT란?

TIME_WAIT은 TCP 통신에서 연결을 종료한 쪽(보통 클라이언트) 이 마지막으로 ACK 응답을 보내고 난 뒤 일정 시간 동안 그 연결 정보를 메모리에 보관하며 대기하는 상태를 의미한다. 이 상태는 TCP의 신뢰성 보장 원칙 때문에 반드시 필요한 과정이며 단순히 소켓을 닫았다고 해서 네트워크 상의 연결이 즉시 사라지는 것이 아니다.

왜 종료 후에도 기다릴까?

TCP는 데이터의 정확한 순서 보장, 손실 없는 전송, 중복 방지 등의 특성을 제공한다. 이런 특성들을 유지하기 위해 연결을 끊는 과정에서도 마지막 패킷이 제대로 도달했는지 확인하고 지연된 패킷이나 재전송 요청에 대응할 준비를 해야 한다.

예를 들어 클라이언트가 마지막 ACK 패킷을 서버로 전송했는데 해당 패킷이 중간에서 유실되면 서버는 재전송을 시도할 수 있다. 이때 클라이언트가 이미 연결 정보를 지워버렸다면 해당 재전송에 응답할 수 없게 되며 TCP의 신뢰성 보장도 깨지게 된다. 그래서 2 * MSL(Maximum Segment Lifetime) 시간 동안 클라이언트는 TIME_WAIT 상태로 대기하며 패킷 재전송에 응답할 준비를 유지하는 것이다.

왜 TIME_WAIT이 발생할까?

TCP는 연결을 끊는 과정에서도 4-way handshake라는 정해진 절차를 따른다. 이 과정에서 마지막 ACK를 보낸 측이 TIME_WAIT 상태로 진입하게 된다.

동작과정

  1. 클라이언트 -> 서버 : FIN 전송 (연결 종료 요청)
  2. 서버 -> 클라이언트 : ACK 응답 (요청 수락)
  3. 서버 -> 클라이언트 : FIN 전송 (서버도 종료 의사 전달)
  4. 클라이언트 -> 서버 : ACK 응답 (서버 종료 확인)
  5. 클라이언트 : TIME_WAIT 상태 진입 (지연 패킷 대비 대기)

마지막 ACK 이후 왜 대기할까?

클라이언트가 보낸 마지막 ACK가 중간에 손실되었을 경우를 대비해 서버는 일정 시간 내에 해당 ACK가 오지 않으면 자신이 보냈던 FIN 패킷을 다시 전송한다. 이때 클라이언트가 응답할 수 있어야 TCP의 종료 절차가 완전히 마무리된다. 또한, 이전 연결에서 떠돌던 지연된 패킷이 새로운 연결에 혼입되는 문제를 방지하기 위해 클라이언트는 MSL의 2배 시간 동안 기다리게 된다.


서버에서 TIME_WAIT은 어떤 문제가 되는가?

다중 접속 처리 시 문제

서버 또는 클라이언트가 아주 짧은 시간 동안 수많은 TCP 연결을 맺고 끊는 경우 TIME_WAIT 상태가 빠르게 누적되어 시스템 자원이 고갈되는 문제가 발생할 수 있다. 특히 웹 서버나 API 서버처럼 다수의 클라이언트가 빠르게 요청을 보내고 응답을 받은 뒤 연결을 종료하는 구조에서는 더욱 심각하게 나타난다.

예시 시나리오

  1. 서버가 클라이언트 요청을 받고 응답한 뒤 연결을 끊는다.
  2. 수많은 클라이언트가 동시다발적으로 재요청한다.
  3. 이전 연결이 TIME_WAIT 상태로 유지되고 있어 같은 포트를 재사용할 수 없다.

이로 인해 다음과 같은 문제가 발생한다.

  • 포트 고갈: 한정된 범위 내에서 ephemeral port(임시 포트)를 다 써버림 (65535개 한도)
  • Connection refused, BindException: Address already in use 오류
  • 응답 지연 및 연결 실패 빈도 증가
  • 서버 부하 증가 -> 리소스 낭비 -> 성능 저하

실제로 netstat, ss 등의 명령어를 사용해 보면 TIME_WAIT 상태의 소켓이 수천 개 이상 잡혀있는 것을 확인할 수 있다.

# TIME_WAIT 상태 확인
netstat -an | grep TIME_WAIT | wc -l

해결 방법

1. SO_REUSEADDR, SO_REUSEPORT 옵션 사용

소켓을 열 때 TIME_WAIT 상태에서도 포트를 재사용할 수 있도록 설정하는 옵션이다.

ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);
  • SO_REUSEADDR: TIME_WAIT 중에도 포트를 바인딩 가능하게 함
  • SO_REUSEPORT: 다수의 소켓이 같은 포트 번호로 바인딩 가능 (Linux 커널 3.9+)

⚠️ 주의: 포트를 재사용하면 이전 연결의 잔류 패킷이 새 연결로 들어올 위험이 있으므로 신중하게 사용해야 한다.

2. keep-alive 활용 (HTTP)

HTTP 1.0 시절에는 요청마다 TCP 연결을 새로 맺고 끊는 구조였기 때문에 TIME_WAIT이 매우 자주 발생했다. 하지만 HTTP/1.1부터는 기본적으로 Connection: keep-alive 옵션이 적용되어 하나의 TCP 연결을 여러 요청에서 재사용하도록 개선되었다.

Connection: keep-alive
  • 동일 연결 유지 -> 새로운 연결 생성 감소
  • TIME_WAIT 상태도 자연스럽게 감소
  • 실제 운영 환경에서는 연결 유지 시간, 요청 수 제한 등을 설정하여 최적화 가능

예: Nginx의 keepalive_timeout, Apache의 KeepAliveTimeout 설정 등

3. 서버에서 FIN을 먼저 보내지 않도록 구조 설계

TCP 종료는 FIN을 먼저 보낸 쪽이 마지막 ACK를 보내므로 TIME_WAIT에 빠진다. 따라서 클라이언트가 먼저 FIN을 보내게 하면 서버는 마지막 ACK만 보내게 되어 TIME_WAIT 상태에 빠지지 않는다. 서버에서 연결 종료를 지연하거나 애플리케이션 레벨에서 클라이언트가 먼저 close() 하도록 유도하는 설계

4. 커널 파라미터 조정 (Linux)

운영 서버에서는 커널 파라미터를 조정해 TIME_WAIT 상태의 지속 시간을 줄이거나 재사용을 허용할 수 있다.

# TIME_WAIT 유지 시간 조절
sysctl -w net.ipv4.tcp_fin_timeout=30

# TIME_WAIT 상태의 포트 재사용 허용
sysctl -w net.ipv4.tcp_tw_reuse=1

# 빠른 회수 허용 (NAT 환경에서는 권장하지 않음

sysctl -w net.ipv4.tcp_tw_recycle=1
  • tcp_fin_timeout: TIME_WAIT 상태 유지 시간 (기본 60초 -> 30초)
  • tcp_tw_reuse: TIME_WAIT 소켓을 재사용할 수 있도록 허용 (주로 클라이언트용)
  • tcp_tw_recycle: 빠르게 회수하지만 NAT 환경에서 문제 발생 가능

결론

TIME_WAIT은 TCP의 신뢰성 보장을 위한 필수 안전장치다. 하지만 짧은 시간 동안 많은 연결을 맺고 끊는 구조에서는 자원 고갈 및 연결 실패 등 다양한 문제를 유발할 수 있다. 이를 해결하기 위해 상황에 맞게 다음과 같은 방법을 조합해서 사용해야 한다.

  • 포트 재사용 (SO_REUSEADDR, SO_REUSEPORT)
  • HTTP keep-alive 설정
  • 서버가 FIN을 먼저 보내지 않도록 구조 설계
  • 커널 파라미터 조정 (tcp_fin_timeout, tcp_tw_reuse 등)

특히 백엔드 서버가 외부 API 호출 등 클라이언트 역할을 수행할 경우 TIME_WAIT은 서버에 집중적으로 누적되어 장애 원인이 될 수 있으므로 반드시 주의가 필요하다.

📝 배운점

이걸 공부하면서 TCP가 얼마나 신중하게 "연결을 끊는지" 알 수 있었다. 처음엔 단순히 socket.close()만 호출하면 끝나는 줄 알았는데 실제로는 그 뒤에도 수 초에서 수 분간 시스템 내부에서 연결 정보가 살아 있으며 네트워크 상의 모든 예외 상황을 고려해 기다리고 있는 구조였다. 운영 서버에서는 "왜 갑자기 포트가 부족하지?", "연결이 안 돼?" 같은 문제가 종종 발생하는데 알고 보니 원인은 대부분 TIME_WAIT이었다. 특히 서버가 백엔드 간 통신에서 클라이언트 역할을 병행할 때 이 문제가 부각되며 소켓 재사용이나 keep-alive 미적용 시 심각한 장애로 이어질 수 있었다. 이후 SO_REUSEADDR, keep-alive, 커널 파라미터 조정 등을 통해 문제를 해결하며 단순히 애플리케이션 로직이 아닌 네트워크 레벨의 동작과 자원까지 고려한 설계가 얼마나 중요한지 깨닫게 되었다. 앞으로는 시스템을 설계할 때도 기능 구현뿐 아니라 운영 중 발생할 수 있는 연결 문제와 리소스 관리까지 고려한 구조를 만드는 것이 중요하다는 점을 다시 한 번 알 수 있었다.

profile
개발자가 되고 싶은 취준생

0개의 댓글