시스템 관리_26 아파치 톰캣 Address already in use bind 포트 충돌 오류 원인과 완벽 해결 가이드

 
  • 출처 / 참고: Linux man-pages (bind(2), ip(7)), Apache Tomcat Configuration Reference, Apache HTTP Server Documentation

명령어: ss, lsof, fuser, netstat, ps, kill, systemctl
키워드: Address already in use, bind, Port Conflict, Apache, Tomcat, 8080, 80, 8005, TIME_WAIT, Zombie Process
사용처: 웹 서버(Apache) 및 WAS(Tomcat) 기동 실패 시 동일 포트 중복 점유 프로세스 식별 및 강제 종료, 포트 충돌 방지 튜닝


실행예제

아파치(Apache) 또는 톰캣(Tomcat) 기동 시 Address already in use: bind 오류가 발생하며 데몬이 비정상 종료될 때, 해당 포트를 선점하고 있는 프로세스(PID)와 잔존 세션을 확인하는 명령어입니다.

# 1. 톰캣 catalina.out 또는 아파치 error.log에서 포트 바인딩 실패 확인
$ tail -n 30 /opt/tomcat/logs/catalina.out
# [출력 예시 - Tomcat 8080 포트 또는 8005 셧다운 포트 충돌]
# SEVERE [main] org.apache.catalina.core.StandardServer.await Failed to create server shutdown socket on address [localhost] and port [8005]
# java.net.BindException: Address already in use (Bind failed)
# 또는
# org.apache.catalina.LifecycleException: Protocol handler start failed
# Caused by: java.net.BindException: Address already in use: bind <null>:8080

# [출력 예시 - Apache 80 또는 443 포트 충돌]
# (98)Address already in use: AH00072: make_sock: could not bind to address 0.0.0.0:80
# no listening sockets available, shutting down

# 2. 문제가 되는 포트(예: 80, 8080, 8005)를 점유하고 있는 프로세스(PID) 및 프로그램명 확인
$ sudo ss -tulpn | grep -E ':(80|8080|8005)\s'
# tcp   LISTEN 0      100        0.0.0.0:8080       0.0.0.0:*    users:(("java",pid=15420,fd=45))
# tcp   LISTEN 0      511        0.0.0.0:80         0.0.0.0:*    users:(("nginx",pid=1120,fd=6))

# 3. lsof 명령어로 충돌 포트를 물고 있는 정확한 프로세스 상세 정보 추출
$ sudo lsof -i :8080
# COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
# java    15420 tomcat   45u  IPv6 125432      0t0  TCP *:8080 (LISTEN)

# 4. fuser 명령어를 사용하여 해당 포트를 잡고 있는 PID 직접 조회
$ sudo fuser 8080/tcp
# 8080/tcp:            15420

# 5. 기존 인스턴스가 좀비/행(Hang) 상태로 남아있는지 프로세스 목록 조회
$ ps -ef | grep -E 'tomcat|httpd|apache2' | grep -v grep

   

스크립트

지정한 포트를 현재 점유 중인 프로세스를 정밀 탐색하여 일반 종료(SIGTERM)를 시도하고, 응답하지 않을 경우 강제 종료(SIGKILL)한 뒤 포트 해제 여부를 확인하는 복구 쉘 스크립트입니다.

#!/bin/bash
# resolve_port_conflict.sh - Find and terminate processes causing "Address already in use: bind"

TARGET_PORT=${1:-"8080"}

if [ "$EUID" -ne 0 ]; then
    echo "오류: 포트 점유 프로세스를 조회 및 종료하려면 루트(root) 권한이 필요합니다."
    echo "사용법: sudo $0 [포트번호]"
    exit 1
fi

echo "=========================================="
echo " [포트 충돌 진단 및 해제] 대상 포트: ${TARGET_PORT}"
echo "=========================================="

# 1. 포트 점유 여부 확인
PID_LIST=$(lsof -t -i :"$TARGET_PORT" 2>/dev/null)

if [ -z "$PID_LIST" ]; then
    echo "  [정상] 포트 ${TARGET_PORT}번은 현재 비어 있습니다 (충돌 없음)."
    
    # TIME_WAIT 상태 소켓 여부 체크
    TW_COUNT=$(ss -tan state time-wait | grep -c ":${TARGET_PORT}\s")
    if [ "$TW_COUNT" -gt 0 ]; then
        echo "  [참고] 프로세스는 없으나 TIME_WAIT 소켓이 ${TW_COUNT}개 남아 있습니다."
        echo "  -> SO_REUSEADDR 미적용 시 일시적 바인딩 지연이 발생할 수 있습니다."
    fi
    exit 0
fi

echo "  [경고] 포트 ${TARGET_PORT}번을 점유 중인 프로세스 감지:"
for PID in $PID_LIST; do
    PROC_NAME=$(ps -p "$PID" -o comm= 2>/dev/null)
    PROC_CMD=$(ps -p "$PID" -o args= 2>/dev/null | cut -c 1-80)
    echo "  -> PID: ${PID} | 명령: ${PROC_NAME} (${PROC_CMD}...)"
done

# 2. 프로세스 안전 종료 (SIGTERM)
echo -e "\n[1] 프로세스 정상 종료 시도 (SIGTERM)..."
kill -15 $PID_LIST 2>/dev/null
sleep 2

# 3. 종료 확인 및 강제 종료 (SIGKILL)
STILL_RUNNING=$(lsof -t -i :"$TARGET_PORT" 2>/dev/null)
if [ -n "$STILL_RUNNING" ]; then
    echo "  [주의] 프로세스가 2초 내에 종료되지 않았습니다. 강제 종료(SIGKILL)를 수행합니다."
    kill -9 $STILL_RUNNING 2>/dev/null
    sleep 1
fi

# 4. 최종 결과 검증
FINAL_CHECK=$(lsof -t -i :"$TARGET_PORT" 2>/dev/null)
if [ -z "$FINAL_CHECK" ]; then
    echo -e "\n[성공] 포트 ${TARGET_PORT}번이 정상적으로 해제되었습니다."
    echo "이제 아파치 또는 톰캣 서비스를 다시 기동할 수 있습니다."
else
    echo -e "\n[실패] 포트 ${TARGET_PORT}번 해제에 실패했습니다. PID: ${FINAL_CHECK}"
fi

echo "=========================================="
echo " [진단 및 조치 완료]"
echo "=========================================="

   

해설

java.net.BindException: Address already in use 또는 아파치의 AH00072: make_sock: could not bind to address 오류는 커널에 bind() 시스템 콜을 호출하여 특정 IP와 포트 번호 조합의 TCP 리스닝 소켓을 열려고 시도했으나, 이미 동일한 포트 번호가 다른 프로세스에 바인딩되어 있거나 커널 소켓 테이블에 반환되지 않은 상태일 때 발생합니다.

1. 주요 발생 원인

  1. 톰캣 인스턴스의 비정상 종료 (가장 빈번함):
    • shutdown.sh를 실행했으나 백그라운드 스레드(DB 커넥션 풀, 배치 스레드)가 종료되지 않아 이전 톰캣 JVM 프로세스가 백그라운드에 그대로 살아있는 상태에서 새로 startup.sh를 호출한 경우.
  2. 이종 웹/WAS 서버 간 기본 포트 충돌:
    • 시스템에 Nginx가 80번을 쓰고 있는데 Apache HTTPD를 기동하려 하거나, Jenkins/다른 스프링 부트 애플리케이션이 이미 8080번을 점유하고 있는 경우.
  3. 톰캣 관리 포트(8005, 8009) 충돌:
    • 한 서버에서 여러 개의 톰캣 인스턴스(Multi-Instance)를 구동할 때, server.xml의 HTTP 포트(8080)만 바꾸고 Shutdown 포트(8005)나 AJP 포트(8009)를 수정하지 않고 띄운 경우.
  4. TIME_WAIT 상태의 소켓 점유:
    • 프로세스가 강제 종료된 직후 클라이언트 세션이 TIME_WAIT 상태로 남아 있고, 서버 애플리케이션 소켓에 SO_REUSEADDR 옵션이 설정되어 있지 않아 2MSL(약 60초) 동안 포트 재바인딩이 거부되는 경우.

2. 단계별 해결 방법

1) 기존 좀비 프로세스 정리 후 재기동

기존 프로세스를 명확히 종료하고 톰캣을 띄워야 합니다.

# fuser 명령어로 8080 포트를 점유한 프로세스를 즉시 SIGKILL 종료
sudo fuser -k -9 8080/tcp
sudo fuser -k -9 8005/tcp

# 이후 톰캣 재시작
/opt/tomcat/bin/startup.sh

2) server.xml 포트 변경 (멀티 인스턴스 환경)

여러 개의 톰캣을 띄워야 한다면 conf/server.xml에서 세 가지 포트를 모두 고유한 값으로 변경해야 합니다.

<!-- 1. Shutdown 포트 변경 -->
<Server port="8006" shutdown="SHUTDOWN">

  <!-- 2. HTTP 커넥터 포트 변경 -->
  <Service name="Catalina">
    <Connector port="8081" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8444" />

    <!-- 3. AJP 커넥터 포트 변경 (사용하지 않을 경우 주석 처리 권장) -->
    <!-- <Connector port="8010" protocol="AJP/1.3" redirectPort="8444" /> -->
  </Service>
</Server>

3) systemd 유닛 파일을 통한 안전한 생명주기 관리

startup.sh와 shutdown.sh를 직접 쓰면 프로세스 누락이 자주 발생하므로, systemd 서비스 유닛을 작성하여 PID 파일 기반 자동 회수를 구성합니다.

# /etc/systemd/system/tomcat.service
[Service]
Type=forking
User=tomcat
Group=tomcat
Environment="CATALINA_PID=/opt/tomcat/temp/tomcat.pid"
ExecStart=/opt/tomcat/bin/startup.sh
ExecStop=/opt/tomcat/bin/shutdown.sh 10 -force
Restart=on-failure
  • shutdown.sh 10 -force 옵션은 10초 내로 정상 종료되지 않을 경우 자동으로 kill -9를 수행하여 포트 선점 문제를 예방합니다.

   

주의사항

  1. 무차별 kill -9 남발 주의:
    • 포트가 잡혀있다고 해서 즉시 kill -9를 때리면 DB 트랜잭션 롤백 누락, 캐시 파일 손상, 버퍼 플러시 미수행 등의 데이터 정합성 문제가 생길 수 있습니다. 우선 kill -15 (SIGTERM)로 정상 종료를 유도하고 타임아웃 후에 강제 종료해야 합니다.
  2. IPv4와 IPv6 이중 바인딩 혼선:
    • ss -tulpn 확인 시 :::8080 (IPv6) 형태로만 떠 있는 경우가 있습니다. 리눅스 기본 커널 설정상 IPv6 와일드카드 바인딩은 IPv4(0.0.0.0) 요청도 함께 처리(Dual-stack)하므로, IPv4 전용 프로세스가 같은 포트로 뜨려고 하면 포트 충돌이 발생합니다. JVM 인자에 -Djava.net.preferIPv4Stack=true를 주어 바인딩 체계를 일치시키는 것이 안전합니다.
  3. Apache 중복 Listen 지시어 확인:
    • 아파치 설정에서 ports.conf와 sites-available/*.conf 양쪽에 Listen 80 지시어가 중복 선언되어 있으면 아파치 스스로가 포트 충돌을 일으키며 구동을 거부할 수 있으므로 중복 선언 여부를 확인해야 합니다.