포트 충돌(이미 사용 중인 포트) 오류 해결하기

서버를 띄우려는데 EADDRINUSE 한 줄과 함께 프로세스가 죽습니다. 어제까지 잘 되던 3000번 포트가 오늘은 이미 쓰이고 있다고 합니다. 포트 충돌 해결은 대부분 3분이면 끝나는 일인데, 방법을 모르면 컴퓨터를 재부팅하는 것으로 마무리하게 됩니다. 하루에 한두 번씩 개발 서버를 재시작하는 사람이라면 이 재부팅 습관만으로 한 달에 두세 시간이 사라집니다.

이 글에서는 어떤 프로세스가 포트를 물고 있는지 찾는 명령부터, 프로세스를 죽였는데도 계속 실패하는 TIME_WAIT 상황, 윈도우에서만 나타나는 예약 포트 함정, 그리고 애초에 충돌이 안 생기게 만드는 습관까지 순서대로 정리합니다.

오류 메시지부터 제대로 읽기

언어와 플랫폼마다 문구가 달라서 같은 문제인 줄 모르고 헤매는 경우가 많습니다. 아래는 모두 동일한 원인입니다.

# Node.js
Error: listen EADDRINUSE: address already in use :::3000

# Python
OSError: [Errno 98] Address already in use

# 윈도우 (Go, .NET 등)
listen tcp 0.0.0.0:8080: bind: Only one usage of each socket address
(protocol/network address/port) is normally permitted.

# Docker
Error response from daemon: driver failed programming external connectivity
on endpoint web: Bind for 0.0.0.0:5432 failed: port is already allocated
파이썬에서 이미 사용 중인 8765 포트에 bind를 시도해 OSError WinError 10048이 발생한 터미널 화면
같은 포트에 두 번 bind하면 이렇게 끝납니다. 메시지에 포트 번호가 안 나오는 것이 이 오류를 헷갈리게 만드는 지점입니다.

핵심은 마지막에 찍힌 포트 번호입니다. :::3000은 IPv6를 포함한 모든 인터페이스의 3000번, 0.0.0.0:8080은 IPv4 전체 인터페이스의 8080번을 뜻합니다. 이 번호만 확보하면 나머지는 기계적인 작업입니다.

1단계 — 누가 그 포트를 쓰고 있는지 찾기

netstat -ano 명령으로 8765 포트를 점유한 프로세스의 PID를 확인한 터미널 화면
맨 오른쪽 숫자가 PID입니다. 이 값을 가지고 다음 단계로 넘어갑니다.

바로 죽이지 말고 무슨 프로그램인지부터 확인하세요. PID만 보고 종료했다가 실행 중이던 데이터베이스나 회사 VPN 클라이언트를 내려버리는 일이 실제로 자주 생깁니다.

윈도우 (명령 프롬프트)

:: 1) 3000번 포트를 물고 있는 프로세스의 PID 찾기
netstat -ano | findstr :3000

:: 출력 예시 — 맨 오른쪽 숫자가 PID입니다
:: TCP    0.0.0.0:3000    0.0.0.0:0    LISTENING    18244

:: 2) 그 PID가 무슨 프로그램인지 확인 (이게 중요합니다)
tasklist /FI "PID eq 18244"

:: 3) 확인했으면 종료
taskkill /PID 18244 /F

윈도우 (PowerShell)

PowerShell을 쓴다면 Get-NetTCPConnection이 훨씬 편합니다. PID를 눈으로 옮겨 적을 필요 없이 프로세스 이름까지 한 번에 나옵니다.

# 포트를 점유한 프로세스를 이름까지 한 번에 보기
Get-NetTCPConnection -LocalPort 3000 -State Listen |
  Select-Object LocalAddress, LocalPort, OwningProcess,
    @{ n = "Process"; e = { (Get-Process -Id $_.OwningProcess).ProcessName } }

# 확인한 뒤 종료
Get-NetTCPConnection -LocalPort 3000 -State Listen |
  ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }

macOS / 리눅스

# macOS / 리눅스 — 누가 쓰는지 보기
lsof -i :3000

# 리눅스에서 lsof가 없다면
ss -ltnp | grep :3000

# PID만 뽑아서 종료 (확인 후에!)
lsof -ti:3000 | xargs kill

# 그래도 안 죽으면 마지막 수단
lsof -ti:3000 | xargs kill -9

kill -9는 프로세스가 정리 작업을 할 틈도 주지 않고 강제 종료합니다. 데이터베이스나 쓰기 작업 중인 프로세스에 쓰면 데이터가 깨질 수 있으니, 먼저 일반 kill로 시도하고 반응이 없을 때만 쓰세요.

2단계 — 죽였는데도 “이미 사용 중”이라고 할 때

프로세스 목록에 아무것도 없는데 계속 EADDRINUSE가 나는 경우가 있습니다. 십중팔구 TIME_WAIT 상태입니다. TCP 연결을 먼저 닫은 쪽은 뒤늦게 도착할 수 있는 패킷을 처리하려고 소켓을 일정 시간 붙잡아 둡니다. 리눅스는 이 시간이 60초로 고정돼 있고, 윈도우는 기본값이 더 길게 잡혀 있습니다.

현재 상태는 이렇게 확인합니다. 상태 열에 TIME_WAIT가 보이면 기다리거나, 아래 옵션을 켜면 됩니다.

netstat -ano | findstr :3000

# TCP    127.0.0.1:3000    127.0.0.1:54120    TIME_WAIT    0

해결책은 소켓에 SO_REUSEADDR를 켜는 것입니다. 이 옵션이 있으면 TIME_WAIT에 걸린 주소에도 곧바로 bind할 수 있습니다.

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 이 한 줄이 TIME_WAIT 때문에 못 뜨는 문제를 막아줍니다.
# 반드시 bind() 앞에 와야 합니다.
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s.bind(("0.0.0.0", 8000))
s.listen(5)
print("listening on 8000")

프레임워크별로 기본값이 다릅니다. 내가 쓰는 도구가 어느 쪽인지 알아두면 헛고생을 줄일 수 있습니다.

환경SO_REUSEADDR 기본값대처
Node.js (net/http)켜짐별도 설정 불필요
Python socket 직접 사용꺼짐setsockopt로 직접 켜기
Python http.server켜짐기본 클래스가 이미 설정해 둠
Python socketserver.TCPServer꺼짐allow_reuse_address = True 지정
Go net.Listen켜짐별도 설정 불필요

3단계 — 윈도우에만 있는 예약 포트 함정

가장 사람을 지치게 하는 유형입니다. netstat에도 아무것도 없고, 재부팅도 했는데, 특정 포트만 계속 bind에 실패합니다. Hyper-V, WSL2, Docker Desktop을 쓰는 환경에서 자주 나타납니다. 윈도우가 이 기능들을 위해 포트 구간을 통째로 예약해 버리는데, 예약된 구간은 사용 중이 아닌데도 일반 프로그램이 쓸 수 없습니다.

:: 윈도우가 미리 예약해서 아무도 못 쓰는 포트 구간 확인
netsh interface ipv4 show excludedportrange protocol=tcp

:: 출력 예시 — 이 구간 안의 포트는 사용 중이 아닌데도 bind가 실패합니다
:: 시작 포트    끝 포트
:: ----------   --------
::       1243       1342
::      50000      50059

:: 관리자 권한 프롬프트에서 winnat을 재시작하면 예약이 재배치됩니다
net stop winnat
net start winnat

출력된 구간 안에 내 포트가 들어 있다면 그게 원인입니다. winnat 재시작으로 풀리는 경우가 많지만, 재부팅하면 다시 같은 구간이 잡힐 수 있습니다. 매번 겪는다면 아예 포트 번호를 바꾸는 편이 빠릅니다. 참고로 윈도우가 임시 포트로 쓰는 구간은 아래로 확인할 수 있는데, 개발 서버 포트는 이 구간을 피해서 고르는 게 좋습니다.

netsh int ipv4 show dynamicport tcp

:: 시작 포트   : 49152
:: 포트 수     : 16384

애초에 충돌이 안 생기게 만드는 습관

매번 프로세스를 찾아 죽이는 것보다, 충돌할 일이 없게 만드는 쪽이 훨씬 낫습니다. 비용이 거의 들지 않는 세 가지입니다.

1) 포트를 코드에 박지 않습니다

// Node.js — 포트를 코드에 박지 않습니다
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log("http://localhost:" + PORT));

// 실행할 때만 바꿔치기
// PORT=3100 npm start          (macOS / 리눅스)
// $env:PORT=3100; npm start    (PowerShell)

환경변수로 빼두면 충돌이 났을 때 코드 수정 없이 다른 포트로 즉시 우회할 수 있습니다. 프로젝트마다 포트를 미리 정해 .env에 적어두면 서로 겹칠 일도 없습니다.

2) 테스트에서는 포트 0을 씁니다

자동화된 테스트가 고정 포트를 쓰면 개발 서버와 부딪히고, 병렬 실행 시 테스트끼리도 부딪힙니다. 포트 0을 넘기면 운영체제가 비어 있는 포트를 골라줍니다.

import socket

# 포트 0으로 bind하면 운영체제가 비어 있는 포트를 알아서 골라줍니다.
# 테스트 코드에서 포트 충돌을 원천 차단하는 표준적인 방법입니다.
s = socket.socket()
s.bind(("127.0.0.1", 0))
print("할당받은 포트:", s.getsockname()[1])   # 예: 54731

3) 포트 비우는 스크립트를 하나 만들어 둡니다

같은 명령을 매번 손으로 치는 대신 스크립트로 만들어 두면, 서버를 띄우는 npm 스크립트 앞에 붙여 자동 실행할 수도 있습니다.

# free-port.ps1 — 인자로 받은 포트를 비우는 스크립트
param([Parameter(Mandatory = $true)][int]$Port)

$conns = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue

if (-not $conns) {
    Write-Host "$Port 번 포트는 비어 있습니다."
    return
}

foreach ($c in $conns) {
    $proc = Get-Process -Id $c.OwningProcess -ErrorAction SilentlyContinue
    Write-Host "종료: $($proc.ProcessName) (PID $($c.OwningProcess))"
    Stop-Process -Id $c.OwningProcess -Force
}

# 사용법:  powershell -ExecutionPolicy Bypass -File free-port.ps1 -Port 3000

Node 환경이라면 설치 없이 npx kill-port 3000 한 줄로도 같은 일을 할 수 있습니다. 다만 이건 확인 없이 바로 죽이므로, 무엇이 물고 있는지 모르는 상태에서는 쓰지 마세요.

주의할 점

  • PID를 확인하지 않고 종료하지 않습니다. 같은 포트를 시스템 서비스가 쓰고 있을 수 있고, 윈도우에서 PID 4는 커널 프로세스라 종료하면 안 됩니다.
  • 1024번 미만 포트는 관리자 권한이 필요합니다. macOS와 리눅스에서 80·443 포트에 bind하려면 sudo가 필요합니다. 개발 중에는 3000·8080처럼 높은 번호를 쓰는 편이 안전합니다.
  • 포트 번호가 같아도 인터페이스가 다르면 충돌하지 않습니다. 127.0.0.1:3000과 0.0.0.0:3000은 다릅니다. 후자는 전자를 포함하므로, 이미 0.0.0.0으로 떠 있으면 127.0.0.1로도 뜰 수 없습니다.
  • 도커 컨테이너는 호스트 명령으로 안 잡힙니다. port is already allocated가 나오면 docker ps로 포트 매핑을 확인하고 해당 컨테이너를 내려야 합니다.
  • 재부팅은 해결이 아니라 회피입니다. 같은 문제가 반복된다면 원인이 되는 프로그램(자동 실행되는 개발 서버, 종료되지 않는 디버거 등)을 찾아 두는 편이 장기적으로 이깁니다.

마무리

당장 할 일은 하나입니다. 자주 쓰는 포트 번호로 netstat -ano | findstr :3000(또는 lsof -i :3000)을 한 번 실행해 보세요. 지금 무엇이 떠 있는지 눈으로 확인하는 것만으로도, 다음에 같은 오류를 만났을 때 재부팅 대신 이 명령이 먼저 떠오릅니다. 여유가 되면 위의 free-port 스크립트를 프로젝트 폴더에 하나 넣어두시길 권합니다.

다음 글에서는 셀레니움과 플레이라이트로 웹 폼 입력을 자동화하는 기초를 다룹니다. 매일 같은 사이트에 같은 값을 입력하고 있다면 도움이 될 내용입니다.

댓글 남기기