[카테고리:] 미분류

  • IntelliJ와 Spring Boot: “Port 8080 was already in use” 에러의 심층 분석 및 해결 전략

    안녕하세요, 중급 및 고급 개발자 여러분. 오늘은 IntelliJ에서 Spring Boot 애플리케이션 실행 시 자주 발생하는 “Web server failed to start. Port 8080 was already in use” 에러를 심층적으로 분석하고, 이를 해결하는 프로세스와 kill 명령어의 -9 옵션에 대한 기술적 배경을 다루겠습니다. 이 글은 단순한 해결책을 넘어, 운영체제 수준의 프로세스 관리와 네트워크 포트 충돌의 본질을 이해하고자 하는 분들을 위해 작성되었습니다.


    1. 문제의 본질: 포트 충돌과 네트워크 스택

    Spring Boot 애플리케이션을 실행하려 할 때, 내장 Tomcat 또는 Netty 서버가 기본 포트인 8080을 바인딩하려 하지만, 이미 다른 프로세스가 해당 포트를 점유하고 있다면 아래와 같은 예외가 발생합니다:

    org.springframework.boot.web.server.PortInUseException: Port 8080 is already in use

    TCP/IP 네트워크에서 포트는 로컬 머신의 특정 프로세스와 네트워크 통신을 연결하는 고유 식별자입니다. 운영체제는 동일한 포트를 두 개 이상의 프로세스가 동시에 리스닝(listening) 상태로 사용할 수 없도록 설계되어 있습니다(단, SO_REUSEADDR 옵션을 사용한 예외는 제외). 따라서, 이 에러는 8080 포트가 다른 프로세스에 의해 점유된 상태임을 나타냅니다.


    2. 문제 진단: 포트 점유 프로세스 식별

    포트 충돌을 해결하려면 먼저 8080 포트를 점유하고 있는 프로세스를 식별해야 합니다. 이를 위해 유닉스 계열 시스템(MacOS/Linux)과 Windows에서의 접근법을 살펴보겠습니다.

    2.1. 유닉스 계열: lsofnetstat

    가장 직관적인 도구는 lsof입니다. 다음 명령어를 실행하면 8080 포트를 사용하는 프로세스의 상세 정보를 얻을 수 있습니다:

    lsof -i :8080

    출력 예시:

    COMMAND   PID   USER    FD   TYPE  DEVICE  SIZE/OFF  NODE  NAME
    java      9876  devuser  12u  IPv6  0xabc   0t0      TCP   *:http-alt (LISTEN)
    • PID: 프로세스 ID (위 예시에서는 9876).
    • COMMAND: 실행 중인 명령어 (Java 프로세스).
    • NAME: 포트와 프로토콜 정보 (http-alt는 8080 포트의 별칭).

    lsof가 설치되어 있지 않다면, netstat으로 대체 가능합니다:

    netstat -tuln | grep :8080

    출력 예시:

    tcp6       0      0  :::8080      :::*      LISTEN

    그러나 netstat은 PID를 직접 제공하지 않으므로, lsof와 함께 사용하는 것이 더 효율적입니다.

    2.2. Windows: netstat와 작업 관리자

    Windows에서는 netstat을 사용해 포트 점유를 확인합니다:

    netstat -aon | findstr :8080

    출력 예시:

    TCP    0.0.0.0:8080    0.0.0.0:0    LISTENING    5432

    여기서 5432는 PID입니다. 이를 작업 관리자나 tasklist 명령어로 추가 확인할 수 있습니다:

    tasklist | findstr 5432

    3. 문제 해결: 프로세스 종료와 kill 명령어

    포트를 점유한 프로세스를 식별했다면, 이를 종료해야 합니다. 유닉스 계열 시스템에서는 kill 명령어를 사용하며, Windows에서는 taskkill을 활용합니다.

    3.1. 유닉스: kill과 시그널의 활용

    kill 명령어는 프로세스에 시그널을 보내 종료를 유도합니다. 기본 구문은 다음과 같습니다:

    kill [-signal] <pid>

    주요 시그널

    • SIGTERM (15): 프로세스에 정상적인 종료를 요청. 애플리케이션은 이 시그널을 받아 열린 파일을 닫고, 네트워크 연결을 정리한 후 종료할 수 있습니다.
    • SIGKILL (9): 운영체제가 프로세스를 즉시 종료. 정리 작업 없이 강제로 메모리에서 제거되며, 무조건 종료되지만 부작용(예: 데이터 손실)이 발생할 수 있습니다.

    예제:

    kill -15 9876  # SIGTERM 전송

    5초 후에도 프로세스가 종료되지 않으면:

    kill -9 9876   # SIGKILL 전송

    시그널 처리와 디버깅

    SIGTERM이 무시되는 경우, 해당 프로세스가 시그널 핸들러를 재정의했거나, 좀비/데드락 상태일 가능성이 있습니다. ps 명령어로 상태를 확인할 수 있습니다:

    ps -p 9876 -o pid,state,command

    출력 예시:

    PID   STAT  COMMAND
    9876  S     java -jar myapp.jar
    • S: 인터럽트 가능한 대기 상태.
    • Z: 좀비 상태라면 SIGKILL만 효과적.

    3.2. Windows: taskkill

    Windows에서는 taskkill로 프로세스를 종료합니다:

    taskkill /PID 5432 /F
    • /F: 강제 종료 (SIGKILL과 유사).
    • 생략 시 기본적으로 SIGTERM과 비슷한 동작.

    4. 기술적 심화: kill -9의 위험성과 대안

    4.1. SIGKILL의 작동 원리

    SIGKILL은 운영체제 커널이 프로세스의 실행 컨텍스트를 강제로 해제하고, 할당된 자원을 즉시 회수합니다. 이 과정에서:

    • 열린 파일 디스크립터가 정리되지 않음.
    • 네트워크 소켓이 정상적으로 닫히지 않아 “TIME_WAIT” 상태로 남을 수 있음.
    • 메모리 누수나 데이터베이스 트랜잭션 중단 가능성.

    Spring Boot의 경우, HikariCP 풀링이나 Redis 연결이 비정상 종료되면 이후 재시작 시 추가적인 초기화 오류를 유발할 수 있습니다.

    4.2. 대안 전략

    1. 우선 SIGTERM 시도:
    • 대부분의 JVM 기반 애플리케이션은 SIGTERM을 처리할 수 있는 shutdown hook을 제공합니다.
    • 예: Runtime.getRuntime().addShutdownHook().
    1. Graceful Shutdown:
    • Spring Boot는 /actuator/shutdown 엔드포인트를 통해 우아한 종료를 지원합니다 (management.endpoint.shutdown.enabled=true 설정 필요).
    1. 포트 재사용:
    • SO_REUSEADDR 소켓 옵션을 활성화하여 종료 후 즉시 포트를 재사용 가능:
    ServerSocket server = new ServerSocket();
    server.setReuseAddress(true);
    server.bind(new InetSocketAddress(8080));

    5. 실무 팁: 포트 충돌 예방과 관리

    5.1. 동적 포트 할당

    Spring Boot에서 고정 포트 대신 랜덤 포트를 사용:

    server.port=0

    실행 시 사용 가능한 포트가 자동 할당되며, 로그에서 확인 가능:

    Tomcat started on port(s): 49152 (http)

    5.2. 프로세스 관리 자동화

    포트 충돌을 자동으로 감지하고 해결하는 스크립트:

    #!/bin/bash
    PORT=8080
    PID=$(lsof -t -i :$PORT)
    
    if [ -n "$PID" ]; then
      echo "Port $PORT is in use by PID $PID"
      kill -15 $PID
      sleep 3
      if ps -p $PID > /dev/null; then
        echo "Forcing termination of PID $PID"
        kill -9 $PID
      fi
    else
      echo "Port $PORT is free"
    fi

    5.3. 모니터링 도구

    • Linux: ss 명령어로 실시간 포트 상태 확인 (ss -tuln | grep 8080).
    • 컨테이너 환경: Docker/Kubernetes 사용 시 포트 매핑 충돌 방지.

    6. 결론: 실무에서의 교훈

    “Port 8080 was already in use” 에러는 단순한 문제로 보이지만, 이를 해결하며 우리는 운영체제의 프로세스 관리, 네트워크 스택, 그리고 애플리케이션의 종료 처리에 대한 깊은 이해를 얻을 수 있습니다.

    • 핵심 포인트:
    • lsof/netstat으로 PID 식별, kill -15로 우선 종료 시도, 필요 시 kill -9.
    • SIGKILL은 최후의 수단으로, graceful shutdown을 선호.
    • 포트 충돌을 예방하려면 동적 할당이나 모니터링 도입.
    • 추가 학습:
    • Linux Signals
    • Spring Boot Actuator 문서