iBetter Books
수정

포트가 안 풀려요 — TIME_WAIT

증상

긴급하게 서버를 재시작했습니다. 프로세스를 내리고 다시 올리는 데 5초도 안 걸렸는데, 애플리케이션이 뜨면서 이런 메시지가 나옵니다.

Error: listen EADDRINUSE: address already in use :::8080

분명히 기존 프로세스는 죽었습니다. 그런데 포트를 누군가 붙잡고 있습니다. 당장 서비스를 복구해야 하는 상황에서 이 메시지는 꽤 당혹스럽습니다.

가설

PART 05에서 TCP 연결 종료를 배웠습니다. 4-way 종료가 끝난 뒤에도 연결은 즉시 사라지지 않고 TIME_WAIT 상태로 잠시 남는다고 했습니다. 마지막 ACK가 유실됐을 때를 대비하고, 이전 연결의 떠돌이 패킷이 새 연결에 섞이지 않도록 보호하는 장치입니다.

프로세스가 죽었다고 소켓까지 즉시 사라지지는 않습니다. 운영체제가 TIME_WAIT 상태의 소켓을 일정 시간(보통 2MSL, 약 60초에서 최대 4분) 유지하기 때문입니다.

도구로 확인하기

netstat 또는 ss로 TIME_WAIT 소켓이 실제로 남아 있는지 봅니다.

# 파일: 터미널ss -tan state time-wait

상태 이름을 직접 필터링하는 방식입니다. Linux의 ssnetstat 출력에서 상태 문자열은 밑줄이 아니라 하이픈을 쓴 TIME-WAIT이므로, grep TIME_WAIT처럼 밑줄로 찾으면 아무것도 걸리지 않는 점에 주의합니다. 또한 TIME-WAIT 소켓은 이미 프로세스가 종료된 상태라 -p로 프로세스를 봐도 소유자가 없습니다.

특정 포트만 보고 싶다면 모든 상태를 포함하는 -a로 거릅니다.

# 파일: 터미널ss -tan | grep :8080

TIME-WAIT로 표시된 줄이 있다면 가설이 맞습니다. 해당 소켓이 사라질 때까지 같은 포트로 새 LISTEN 소켓을 열 수 없습니다. 이것이 Address already in use의 실체입니다.

원인과 해결

해결책은 두 가지입니다.

첫 번째는 기다리는 것입니다. TIME_WAIT는 결국 소멸합니다. 긴급하지 않다면 1~2분 후 재시작하면 됩니다.

두 번째는 SO_REUSEADDR 소켓 옵션을 사용하는 것입니다. 이 옵션은 TIME_WAIT 상태의 주소와 포트를 즉시 재사용할 수 있게 허용합니다. 대부분의 서버 프레임워크(Node.js, Spring, FastAPI 등)는 기본적으로 이 옵션을 활성화합니다. 그래서 평소에는 이 문제를 겪지 않다가, 매우 짧은 간격으로 재시작하거나 직접 소켓을 생성하는 코드를 쓸 때 마주칩니다.

Python에서 직접 소켓을 생성한다면 다음처럼 명시적으로 옵션을 설정합니다.

# filename: server.pyimport sockets = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)s.bind(("0.0.0.0", 8080))s.listen()

또는 시스템 레벨에서 TIME_WAIT 재사용을 허용할 수도 있습니다. Linux에서는 아래 설정이 도움이 됩니다.

# 파일: 터미널sysctl net.ipv4.tcp_tw_reuse

값이 0이라면 1로 바꾸면 TIME_WAIT 소켓을 재사용할 수 있습니다. 다만 이 설정은 서버 측이 아닌 아웃바운드 연결을 맺는 클라이언트 측에만 효과가 있습니다.

TIME_WAIT를 바라보는 시각

TIME_WAIT를 버그처럼 느끼기 쉽습니다. 하지만 이것은 TCP가 신뢰성을 지키기 위해 의도적으로 설계한 장치입니다. 문제는 TIME_WAIT 자체가 아니라, 서버 코드에서 SO_REUSEADDR를 설정하지 않은 것입니다. 메커니즘을 이해하면 당황하지 않고 빠르게 대응할 수 있습니다.

다음 절에서는 비슷한 듯 다른 상황, 즉 연결은 되는데 엉뚱한 서버로 가는 문제를 다룹니다.