서버를 껐는데도 EADDRINUSE: address already in use가 계속 난 이유
문제 발생
로컬에서 API 서버를 띄워 놓고 작업하다가 코드를 고쳐서 다시 켜려는데, 서버가 뜨자마자 죽었습니다.
Error: listen EADDRINUSE: address already in use :::3000
at Server.setupListenHandle [as _listen2] (node:net:1940:16)
at listenInCluster (node:net:1997:12)분명히 방금 껐습니다. 터미널에도 프롬프트가 돌아와 있었고요. 그런데 브라우저로 localhost:3000을 열어 보니 에러 페이지도 안 뜨고 하염없이 로딩만 돌았습니다. 꺼진 서버라면 바로 연결 거부가 떠야 하는데, 뭔가가 포트를 쥔 채 대답을 안 하고 있었던 겁니다. 범인을 찾아보니 제가 껐다고 생각한 그 서버였습니다. 터미널 위쪽에 이런 줄이 남아 있었습니다.
[1]+ Stopped node server.jsCtrl+C를 누른다는 게 Ctrl+Z를 누른 거였습니다.
원인 분석
EADDRINUSE는 누군가 이미 그 포트에서 듣고 있다는 뜻입니다. Node.js의 server.listen() 문서는 이걸 listen할 때 가장 흔한 에러로 꼽으면서, 다른 서버가 이미 요청한 포트에서 듣고 있을 때 난다고 설명합니다. 에러 자체는 단순합니다. 어려운 건 "그 누군가"가 누구인지입니다.
Ctrl+Z는 프로세스를 끝내지 않고 멈춰 둡니다. Bash 매뉴얼의 작업 제어 설명에 따르면 실행 중에 suspend 문자(보통 Ctrl+Z)를 누르면 그 프로세스를 멈추고 제어를 Bash로 돌려줍니다. 이후 bg로 백그라운드에서 이어 가거나, fg로 다시 앞으로 가져오거나, kill로 끝낼 수 있다고요. 멈춘 프로세스는 살아 있으니 열어 둔 소켓도 그대로 갖고 있습니다. 직접 재현해 보니 멈춘 Node 프로세스는 상태가 T(stopped)였고, 같은 포트로 다시 띄우면 똑같이 EADDRINUSE가 났습니다. curl로 요청하면 거부당하는 게 아니라 타임아웃까지 기다렸습니다. 운영체제가 연결은 받아 주지만, 그걸 처리할 프로세스가 멈춰 있으니 응답이 오지 않는 겁니다. 브라우저가 로딩만 돌던 이유가 이거였습니다.
:::3000이라고 나와도 IPv4 쪽과 같이 부딪칩니다. 에러 끝의 :::3000은 IPv6의 "모든 주소(::)"의 3000번 포트라는 뜻입니다. Node 문서에 따르면 listen()에 host를 생략하면 IPv6를 쓸 수 있을 때 ::에서, 아니면 0.0.0.0에서 연결을 받고, 대부분의 운영체제에서 ::에서 들으면 0.0.0.0에서도 함께 듣게 될 수 있습니다. 그래서 다른 프로그램이 0.0.0.0:3000을 쥐고 있어도 Node는 :::3000이 이미 쓰인다고 말할 수 있습니다. IPv6를 끈 환경에서 재현했을 때는 같은 에러가 address already in use 0.0.0.0:3000으로 나왔습니다. 주소 표기만 다를 뿐 같은 충돌입니다.
해결 방안
- 먼저 누가 포트를 쥐고 있는지 확인합니다. 추측으로 이것저것 끄기 전에 PID부터 봅니다. Linux와 macOS에서는
lsof로 듣고 있는 프로세스를 찾습니다.
$ lsof -i :3000 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 144 root 21u IPv4 1262 0t0 TCP *:3000 (LISTEN)Windows에서는 netstat -ano | findstr :3000으로 찾습니다. Microsoft 문서대로 -a는 듣고 있는 포트를 포함한 연결을, -n은 주소를 숫자로, -o는 연결마다 PID를 보여 줍니다. 찾은 PID는 taskkill /PID 1234 /F로 끝냅니다. /f는 강제 종료입니다. 이렇게 확인하면 의외의 범인도 보입니다. 예를 들어 macOS에서 Flask 기본 포트 5000이 이미 쓰이고 있다면, Apple의 공식 포트 목록에 5000번과 7000번 TCP가 AirPlay 용도로 올라 있다는 걸 떠올려 볼 만합니다. 이럴 때는 앱 포트를 바꾸는 편이 빠릅니다.
-
Ctrl+Z로 멈춘 서버는 같은 터미널에서 정리합니다. 프롬프트 위에Stopped가 보였다면 그 터미널에서jobs로 멈춘 작업을 확인하고,fg로 다시 가져와Ctrl+C로 끄거나kill %1로 끝냅니다. 여기서 재미있는 차이가 하나 있었습니다. 다른 터미널에서kill <PID>로 SIGTERM을 보내면 멈춘 프로세스는 바로 죽지 않습니다. 직접 해 보니 SIGTERM을 받고도 1초 뒤까지 상태가T그대로였고 포트도 계속 잡혀 있었습니다. SIGCONT로 깨운 순간에야 종료됐습니다. 반면 원래 터미널에서kill %1을 쓰니 바로Terminated가 찍히고 포트가 풀렸습니다. 다른 터미널에서 정리해야 한다면kill -CONT <PID>로 깨운 뒤 끄거나, 정리 코드를 포기할 각오로kill -9(SIGKILL)를 씁니다. SIGKILL은 멈춘 프로세스도 바로 끝냈습니다. -
직접 만든 소켓 서버라면
SO_REUSEADDR를 확인합니다. 프로세스가 진짜로 죽었는데도 잠깐 동안 같은 에러가 나는 경우가 있습니다. Pythonsocket문서의 예제 설명대로, 예제를 짧은 간격으로 여러 번 실행하면OSError: [Errno 98] Address already in use가 날 수 있고, 이건 이전 실행이 소켓을TIME_WAIT상태로 남겨서 바로 재사용할 수 없기 때문입니다. 문서가 제시하는 해법이SO_REUSEADDR입니다.
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))Linux에서 재현해 보니 한 가지 조건이 더 있었습니다. 재시작하는 쪽만 옵션을 켜서는 여전히 Errno 98이 났고, 처음 서버와 재시작한 서버가 둘 다 SO_REUSEADDR를 켰을 때만 바로 bind가 됐습니다. 같은 프로그램을 다시 띄우는 상황이라면 처음부터 코드에 넣어 두면 됩니다. Python 3.8의 socket.create_server()는 문서대로 POSIX에서 이 옵션을 알아서 켜 줍니다. socketserver.TCPServer는 allow_reuse_address가 기본 False라서 직접 켜야 하고, 확인해 보니 http.server.HTTPServer는 이미 켜져 있었습니다. Node는 같은 상황에서 바로 다시 떴습니다. Linux의 socket(7) 매뉴얼대로 이 옵션은 "이미 듣고 있는 소켓"과의 충돌은 막지 않으니, 1번과 2번의 상황은 이걸로 해결되지 않습니다.
-
포트를 알아서 바꾸는 개발 서버를 조심합니다. 모든 도구가
EADDRINUSE로 죽어 주지는 않습니다. Vite 문서에 따르면server.port의 포트가 이미 쓰이고 있으면 다음 빈 포트를 자동으로 시도하기 때문에, 실제로 듣는 포트가 설정과 다를 수 있습니다. 멈춘 개발 서버가 5173을 잡고 있으면 새 서버는 조용히 5174로 뜹니다. 그러면 CORS 허용 출처나 소셜 로그인 리다이렉트 주소가 5173으로 적혀 있는 곳에서 엉뚱한 에러가 납니다. 같은 문서의server.strictPort를true로 주면 포트가 이미 쓰이고 있을 때 다음 포트를 찾지 않고 종료합니다. 포트가 다른 설정과 묶여 있다면 이쪽이 낫습니다. -
코드에서는 재시도보다 분명한 메시지를 남깁니다. Node 문서에는
EADDRINUSE가 나면 잠시 뒤 다시listen하는 예제가 있습니다. 배포 중에 이전 프로세스가 내려가길 기다리는 상황이라면 쓸 만하지만, 로컬 개발에서 무한정 재시도하면 멈춘 서버를 더 오래 못 알아챕니다. 저는 포트 번호와 확인 방법을 출력하고 바로 끝내도록 바꿨습니다.
server.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
console.error(`포트 ${PORT}가 이미 사용 중입니다. lsof -i :${PORT} -sTCP:LISTEN 으로 확인하세요.`);
process.exit(1);
}
throw e;
});포트는 PORT 환경변수로 받게 해 두면, 급할 때 PORT=3001 npm run dev로 비켜 갈 수도 있습니다.
공식 문서
- Node.js: server.listen() — EADDRINUSE
- GNU Bash Manual: Job Control Basics
- Python: socket — Example (SO_REUSEADDR, TIME_WAIT)
- Python: socket.create_server()
- Python: socketserver — allow_reuse_address
- Linux man pages: socket(7) — SO_REUSEADDR
- Vite: server.port, server.strictPort
- Microsoft: netstat
- Microsoft: taskkill
- Apple: TCP and UDP ports used by Apple software products
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.