address already in use 완전 정복: EADDRINUSE·BindException 포트 충돌 해결
배포 버튼을 눌렀는데 빨간 로그 한 줄과 함께 서비스가 안 뜬다. Node는 EADDRINUSE, Java는 java.net.BindException, nginx는 bind() failed라고 외친다. 메시지 모양은 제각각이지만 본질은 "내가 쓰려는 포트를 누군가 이미 잡고 있다" 단 하나다. 이 글은 당황하지 말고 따라 할 수 있도록, 원인을 3가지로 분기하고 OS별 복붙 명령어로 5분 안에 복구하는 절차를 정리했다.
같은 충돌, 다른 에러 메시지
세 런타임이 토해내는 에러는 다음과 같다.
# Node.js
Error: listen EADDRINUSE: address already in use :::3000
# Java
java.net.BindException: Address already in use
# nginx
bind() to 0.0.0.0:80 failed (98: Address already in use)운영체제 레벨에서 보면 모두 bind() 시스템 콜이 EADDRINUSE(errno 98)를 반환한 것이다. 즉 어떤 언어로 짠 서버든 원인 진단 절차는 동일하다는 뜻이다.
에러 메시지별 원인 분기 표
| 에러 메시지 | 가장 흔한 원인 | 두 번째로 의심할 것 |
|---|---|---|
EADDRINUSE :::3000 (Node) | 이전 dev 서버/nodemon이 안 죽고 살아있음 | 핫리로드로 인한 중복 기동 |
java.net.BindException (Java) | 같은 JAR/IDE 인스턴스 중복 실행 | 재기동 시 TIME_WAIT 잔존 + SO_REUSEADDR 미설정 |
bind() to 0.0.0.0:80 failed (98) (nginx) | 이미 떠 있는 nginx/Apache가 80 점유 | 80·443은 root 권한 필요(권한 문제와 혼동 주의) |
원인 3분기 진단 결정 트리
복구 전에 "지금 어떤 케이스인지"부터 판별하자.
- 이미 떠 있는 프로세스가 점유 → 가장 흔하다.
ss/lsof로 PID가 잡힌다. → 해당 프로세스 종료. - 종료된 소켓의 TIME_WAIT 잔존 → 방금 서버를 껐다 바로 켰을 때. PID는 안 잡히는데 포트는 막혀 있다. →
SO_REUSEADDR또는 잠깐 대기. - SO_REUSEADDR 미설정으로 재기동 실패 → 2번과 짝꿍. 재기동 스크립트에서 반복 발생한다면 코드/설정으로 해결.
판별 팁: ss -ltnp로 LISTEN 상태 PID가 보이면 1번, 안 보이는데 ss -tan state time-wait에 해당 포트가 쌓여 있으면 2·3번이다.
OS별 점유 PID 찾아 종료하기 (복붙용)
리눅스
요즘은 netstat보다 ss가 표준이다(net-tools 미설치 서버가 많다).
# 1. LISTEN 중인 프로세스 + PID 확인 (권장)
sudo ss -ltnp | grep :8080
# 2. lsof로 확인
sudo lsof -i :8080
# 3. netstat (구버전 환경)
sudo netstat -tlnp | grep 8080
# 4. 포트 점유 프로세스 한 방에 종료
sudo fuser -k 8080/tcpmacOS
macOS에는 ss와 fuser가 없다. lsof로 찾아 직접 죽인다.
# 점유 PID 확인 (-n: DNS 미조회, -P: 포트번호 그대로)
lsof -nP -i :8080
# 종료
kill -9 <PID>Windows
:: 점유 PID 확인 (맨 오른쪽 열이 PID)
netstat -ano | findstr :8080
:: 강제 종료
taskkill /PID <PID> /F근본 해결과 재발 방지
TIME_WAIT 잔존 다루기
서버를 껐다 바로 켤 때 나는 충돌은 SO_REUSEADDR로 거의 해결된다.
// Node.js — 보통 기본 동작이지만 옵션 객체로 명시 가능
const server = require('http').createServer(app);
server.listen({ port: 3000, host: '0.0.0.0' });// Java 순수 소켓
ServerSocket socket = new ServerSocket();
socket.setReuseAddress(true); // bind 전에 호출해야 함
socket.bind(new InetSocketAddress(8080));Spring Boot는 내장 톰캣이 기본적으로 SO_REUSEADDR를 켜므로, 충돌이 잦다면 application.yml의 포트가 다른 인스턴스와 겹치지 않는지 먼저 확인하자.
# TIME_WAIT 잔존량 확인
ss -tan state time-wait | wc -l# 커널 튜닝 (주의: tcp_tw_reuse는 '클라이언트(아웃바운드)' 측에만 적용됨)
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
# FIN 후 대기 시간 단축
sudo sysctl -w net.ipv4.tcp_fin_timeout=30흔한 오해 한 가지.
tcp_tw_reuse는 LISTEN 서버 포트의 TIME_WAIT를 비워주지 않는다. 서버 측 재기동 충돌의 정답은 커널 튜닝이 아니라SO_REUSEADDR다. 운영에서 무턱대고tcp_tw_reuse를 켰다가 효과 없어 헤맨 사람을 여럿 봤다.
systemd 재기동 시 이전 인스턴스가 안 죽는 케이스
Restart=always인데 메인 프로세스가 자식을 남기고 죽으면, 그 자식이 포트를 쥔 채로 새 인스턴스가 뜨려다 충돌한다. KillMode=mixed로 프로세스 그룹 전체를 정리하게 하자.
[Service]
ExecStart=/usr/bin/node /app/server.js
ExecStop=/bin/kill -SIGTERM $MAINPID
Restart=always
KillMode=mixed # 메인엔 SIGTERM, 나머지 자식엔 SIGKILL
TimeoutStopSec=10
[Install]
WantedBy=multi-user.target# 상태와 로그로 종료 실패 확인
systemctl status myapp
journalctl -u myapp -n 50좀비·고아 프로세스, 그리고 컨테이너
# 앱과 부모 PID(PPID) 추적
ps -ef | grep myappZ 상태(좀비)는 kill -9로 죽지 않는다. 좀비는 부모가 wait()로 회수해야 사라지므로, 부모 프로세스를 종료해야 정리된다. 컨테이너에서 PID 1로 앱을 직접 띄우면 좀비를 회수하지 못해 쌓이는데, --init 옵션이나 tini를 쓰면 해결된다. 쿠버네티스에서는 같은 노드에 hostPort가 겹친 Pod가 스케줄되며 충돌이 나기도 하니, hostPort 대신 Service를 쓰는지 점검하자.
결론: 충돌 발생 시 체크리스트
ss -ltnp | grep :PORT— LISTEN PID가 잡히는가?- 잡히면 → 프로세스 종료(
fuser -k,kill,taskkill) - 안 잡히는데 막혀 있으면 → TIME_WAIT 확인(
ss -tan state time-wait) - 재기동마다 반복되면 →
SO_REUSEADDR설정 / systemdKillMode=mixed - CI·핫리로드에서 dev 서버가 안 죽는다면 → 종료 훅에서 프로세스 그룹 정리
자주 묻는 질문 (FAQ)
Q. kill -9로 죽였는데도 포트가 계속 막혀 있어요.
A. PID가 잡히지 않는다면 프로세스 충돌이 아니라 TIME_WAIT 잔존일 가능성이 큽니다. ss -tan state time-wait로 확인하고, 재기동 충돌이면 SO_REUSEADDR를 켜세요. 보통 수십 초 내 자동 해소됩니다.
Q. macOS에서 ss: command not found가 떠요.
A. macOS에는 ss와 fuser가 없습니다. lsof -nP -i :PORT로 PID를 찾은 뒤 kill -9 <PID>로 종료하세요.
Q. 80·443 포트에서 bind 실패가 나는데 점유 프로세스가 없습니다.
A. 1024 미만 포트는 root 권한이 필요합니다. 권한 문제일 수 있으니 sudo로 실행하거나, 비특권 포트로 띄우고 리버스 프록시로 연결하세요.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...