/개발/EADDRINUSE·BindException 포트 충돌 해결: OS별 PID 종료 명령어
DevelopmentEADDRINUSE포트충돌 해결

EADDRINUSE·BindException 포트 충돌 해결: OS별 PID 종료 명령어

This article is not yet available in English. Showing the original Korean version.

EADDRINUSE, java.net.BindException, nginx bind 실패를 원인별로 진단하고 리눅스·macOS·Windows에서 포트 점유 프로세스를 찾아 종료하는 복붙 명령어와 SO_REUSEADDR·TIME_WAIT 재발 방지법을 정리했습니다.

EADDRINUSE·BindException 포트 충돌 해결: OS별 PID 종료 명령어

address already in use 완전 정복: EADDRINUSE·BindException 포트 충돌 해결

배포 버튼을 눌렀는데 빨간 로그 한 줄과 함께 서비스가 안 뜬다. Node는 EADDRINUSE, Java는 java.net.BindException, nginx는 bind() failed라고 외친다. 메시지 모양은 제각각이지만 본질은 "내가 쓰려는 포트를 누군가 이미 잡고 있다" 단 하나다. 이 글은 당황하지 말고 따라 할 수 있도록, 원인을 3가지로 분기하고 OS별 복붙 명령어로 5분 안에 복구하는 절차를 정리했다.

같은 충돌, 다른 에러 메시지

세 런타임이 토해내는 에러는 다음과 같다.

TEXT
# 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분기 진단 결정 트리

복구 전에 "지금 어떤 케이스인지"부터 판별하자.

  1. 이미 떠 있는 프로세스가 점유 → 가장 흔하다. ss/lsof로 PID가 잡힌다. → 해당 프로세스 종료.
  2. 종료된 소켓의 TIME_WAIT 잔존 → 방금 서버를 껐다 바로 켰을 때. PID는 안 잡히는데 포트는 막혀 있다. → SO_REUSEADDR 또는 잠깐 대기.
  3. SO_REUSEADDR 미설정으로 재기동 실패 → 2번과 짝꿍. 재기동 스크립트에서 반복 발생한다면 코드/설정으로 해결.

판별 팁: ss -ltnpLISTEN 상태 PID가 보이면 1번, 안 보이는데 ss -tan state time-wait에 해당 포트가 쌓여 있으면 2·3번이다.

OS별 점유 PID 찾아 종료하기 (복붙용)

리눅스

요즘은 netstat보다 ss가 표준이다(net-tools 미설치 서버가 많다).

Bash
# 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/tcp

macOS

macOS에는 ssfuser가 없다. lsof로 찾아 직접 죽인다.

Bash
# 점유 PID 확인 (-n: DNS 미조회, -P: 포트번호 그대로)
lsof -nP -i :8080

# 종료
kill -9 <PID>

Windows

CMD
:: 점유 PID 확인 (맨 오른쪽 열이 PID)
netstat -ano | findstr :8080

:: 강제 종료
taskkill /PID <PID> /F

근본 해결과 재발 방지

TIME_WAIT 잔존 다루기

서버를 껐다 바로 켤 때 나는 충돌은 SO_REUSEADDR로 거의 해결된다.

JavaScript
// Node.js — 보통 기본 동작이지만 옵션 객체로 명시 가능
const server = require('http').createServer(app);
server.listen({ port: 3000, host: '0.0.0.0' });
JAVA
// Java 순수 소켓
ServerSocket socket = new ServerSocket();
socket.setReuseAddress(true);   // bind 전에 호출해야 함
socket.bind(new InetSocketAddress(8080));

Spring Boot는 내장 톰캣이 기본적으로 SO_REUSEADDR를 켜므로, 충돌이 잦다면 application.yml의 포트가 다른 인스턴스와 겹치지 않는지 먼저 확인하자.

Bash
# TIME_WAIT 잔존량 확인
ss -tan state time-wait | wc -l
Bash
# 커널 튜닝 (주의: 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로 프로세스 그룹 전체를 정리하게 하자.

INI
[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
Bash
# 상태와 로그로 종료 실패 확인
systemctl status myapp
journalctl -u myapp -n 50

좀비·고아 프로세스, 그리고 컨테이너

Bash
# 앱과 부모 PID(PPID) 추적
ps -ef | grep myapp

Z 상태(좀비)는 kill -9로 죽지 않는다. 좀비는 부모가 wait()로 회수해야 사라지므로, 부모 프로세스를 종료해야 정리된다. 컨테이너에서 PID 1로 앱을 직접 띄우면 좀비를 회수하지 못해 쌓이는데, --init 옵션이나 tini를 쓰면 해결된다. 쿠버네티스에서는 같은 노드에 hostPort가 겹친 Pod가 스케줄되며 충돌이 나기도 하니, hostPort 대신 Service를 쓰는지 점검하자.

결론: 충돌 발생 시 체크리스트

  1. ss -ltnp | grep :PORT — LISTEN PID가 잡히는가?
  2. 잡히면 → 프로세스 종료(fuser -k, kill, taskkill)
  3. 안 잡히는데 막혀 있으면 → TIME_WAIT 확인(ss -tan state time-wait)
  4. 재기동마다 반복되면 → SO_REUSEADDR 설정 / systemd KillMode=mixed
  5. 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에는 ssfuser가 없습니다. lsof -nP -i :PORT로 PID를 찾은 뒤 kill -9 <PID>로 종료하세요.

Q. 80·443 포트에서 bind 실패가 나는데 점유 프로세스가 없습니다. A. 1024 미만 포트는 root 권한이 필요합니다. 권한 문제일 수 있으니 sudo로 실행하거나, 비특권 포트로 띄우고 리버스 프록시로 연결하세요.

✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 ·
관련 공식 문서Oracle Java 공식 문서

Comments

Be the first to comment.