리눅스 Java 설치 및 스프링부트 프로젝트 내장 톰캣 실행 방법

스프링부트 프로젝트는 내장 톰캣이 있으므로, 톰캣을 따로 설치하지 않고 java로 실행할 수 있습니다.


리눅스 Java 설치

리눅스 JDK 압축 파일 다운

https://www.oracle.com/java/technologies/downloads/archive
오라클 로그인 후, 위 경로 우측에서 프로젝트 JDK 버전을 선택합니다.
jdk-17.0.15_linux-x64_bin.tar.gz 같은 압축 파일로 받으면 설치가 간편합니다.
rpm, deb 같은 패키지 설치 방식을 이용하는 것도 가능합니다.

JDK 설치 (압축 파일 해제)

sudo mkdir -p /opt/java
tar -xvzf jdk-17.0.15_linux-x64_bin.tar.gz -C /opt/java

JDK 파일을 파일질라로 리눅스 서버 유저 Home에 올린 뒤, /opt/java 폴더를 생성하고 압축 해제합니다.

Java 실행파일 심볼릭 링크 생성

ln -s /opt/java/jdk-17.0.15/bin/java /usr/bin/java

/usr/bin/java 경로에 java 실행 파일로 연결되는 심볼릭 링크를 생성합니다.

JAVA 버전 확인

java -version

심볼릭 링크 생성 후, 설치된 Java 버전을 확인할 수 있습니다.

Java 환경변수 설정

sudo vi /etc/profile

profile 파일 하단에 아래와 같이 입력 후 저장합니다.

export JAVA_HOME=/opt/java/jdk-17.0.15
export PATH=$JAVA_HOME/bin:$PATH

Java 폴더 경로를 환경변수로 설정합니다.

환경변수 설정 적용

source /etc/profile

profile 파일 변경사항을 적용합니다.

JAVA_HOME 확인

echo $JAVA_HOME

등록된 Java 환경변수 값 (/opt/java/jdk-17.0.15) 을 확인할 수 있습니다.


스프링부트 jar 빌드 방법

IntelliJ 스프링부트 jar 빌드

https://0songha0.github.io/op/2023-03-07-1#intellij-gradle-war-%EB%B9%8C%EB%93%9C-%EB%B0%A9%EB%B2%95
IntelliJ에서 Gradle로 스프링부트 프로젝트 jar 빌드 후, 생성된 .jar 파일을 파일질라로 서버에 올립니다.

프로젝트 배포 폴더 생성

sudo mkdir -p /opt/프로젝트명

프로젝트 jar 파일, deploy.sh 스크립트, 로그 등이 위치할 프로젝트 배포용 폴더를 생성합니다.

jar 파일 이동

cd /home/유저명/deploy
cp ./프로젝트명-GradleVersion명.jar /opt/프로젝트명

파일질라로 올린 jar 파일을 배포 폴더에 복사합니다.


스프링부트 내장 톰캣 서버 실행 방법

deploy.sh 스크립트 또는 systemd 서비스로 스프링부트 프로젝트 실행이 가능합니다.
두 방식 모두 젠킨스와 연동하여 자동 배포 파이프라인을 만들 수 있습니다.

deploy.sh 스크립트 파일 작성 방식

cd /opt/프로젝트명
vi deploy.sh

jar 파일 위치로 이동하고, deploysh 파일 내용을 아래와 같이 작성 후 저장합니다.

#!/bin/bash
set -e  # 에러 발생 시 즉시 종료

# 기존 Java 프로세스 종료
PID=$(pgrep -f java || true)
if [ -n "$PID" ]; then
  kill -9 $PID

  # 종료 확인 (최대 10초)
  for i in {1..10}; do
    if ! ps -p $PID > /dev/null; then break; fi
    sleep 1
  done

  # 종료 실패 시
  if ps -p $PID > /dev/null; then
    echo "기존 프로세스 종료 실패!"
    exit 1
  fi
fi

# 현재 위치에서 최신 JAR 파일 찾기
# ls -t : 수정 시간 기준 내림차순 정렬
APP_NAME=$(ls -t ./*.jar | head -n 1)
if [ -z "$APP_NAME" ]; then
  echo "실행할 JAR 파일을 찾을 수 없습니다."
  exit 1
fi

# 신규 애플리케이션 프로세스 실행
nohup java -Dspring.profiles.active=프로파일명 -jar "$APP_NAME" > /dev/null 2>&1 &
NEW_PID=$!
# /dev/null : 스크립트 로그를 파일로 남기지 않고 버림
# Spring Boot 톰캣 로그는 logback에 의해서 파일로 기록됨

# 실행 확인 (최대 5초)
sleep 3
if ps -p $NEW_PID > /dev/null; then
  echo "애플리케이션 정상 실행 (PID: $NEW_PID)"
else
  echo "애플리케이션 실행 실패 (로그 확인 필요)"
  exit 1
fi

echo "배포 완료"

deploy.sh 스크립트로 실행하는 방식은 서버 재부팅 시 자동 시작이 불가합니다.
운영 환경에서 에러 발생 가능성이 높아 개발, 테스트용으로 적합합니다.

deploy.sh 스크립트 실행 권한 부여 명령어

chmod +x deploy.sh

스크립트 실행 권한을 부여하기 위해 최초 1회만 수행하면 됩니다.
모든 그룹에 대한 스크립트 파일 실행 권한이 부여됩니다.

-bash: ./deploy.sh: Permission denied

실행 권한을 부여하지 않고 실행하면, 위와 같은 메시지가 출력됩니다.

deploy.sh 스크립트 실행 명령어

./deploy.sh

작성한 deploy.sh 스크립트를 실행하여 내장 톰캣을 실행할 수 있습니다.

systemd 서비스 파일 작성 방식

https://0songha0.github.io/op/2022-08-06-1
위 글을 참고하여 서비스 파일을 작성 및 등록하고, systemctl 명령어로 실행하면 됩니다.
서버 재부팅 시 자동 재시작이 가능해 deploy.sh 스크립트 실행 방식보다 운영 환경에 적합합니다.


스프링부트 프로젝트 배포 방법

deploy.sh 스크립트 파일 실행 방식

cd /jar업로드폴더
cp ./프로젝트명-신규GradleVersion명.jar /opt/프로젝트명
./deploy.sh

신규 버전의 jar 파일 업로드 후 deploy.sh 파일을 재실행 합니다.
실행 중 jar 파일을 덮어씌우면 NoClassDefFoundError 에러가 발생할 수 있습니다.

이전 버전 jar 파일 삭제 방법

ls -t /opt/프로젝트명/*.jar | tail -n +4 | xargs rm -f

최신에 올린 jar 3개를 제외하고, 오래된 jar들을 모두 삭제할 수 있는 명령어입니다.
젠킨스 파이프라인에서 배포 완료 후 실행하는 것도 좋습니다.

systemd 서비스 파일 실행 방식

cd /jar업로드폴더
systemctl stop 서비스명.service
cp ./프로젝트명-GradleVersion명.jar /opt/프로젝트명
systemctl start 서비스명.service

기존 서비스 프로세스를 종료하고, jar 파일 교체 후 다시 실행하면 됩니다.


스프링부트 내장 톰캣 로그 관리

톰캣 로그 저장 위치 설정

<property name="LOG_FILE" value="./boot-logs/로그파일명" />

프로젝트 resources 폴더 내 logback-spring.xml 파일에서 스프링부트 로그 파일 위치를 지정할 수 있습니다.

logback-spring.xml 파일 예시

<configuration scan="true" scanPeriod="60 seconds">

    <include resource="org/springframework/boot/logging/logback/defaults.xml" />

    <!-- 스프링부트 jar 파일 위치에서 boot-logs 폴더 하위에 로그파일명.log 파일로 저장됩니다. -->
    <property name="LOG_FILE" value="./boot-logs/로그파일명" />

    <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <springProfile name="local">
        <appender name="rollingFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <encoder>
                <charset>UTF-8</charset>
                <!-- <pattern>${FILE_LOG_PATTERN}</pattern>  -->
                <pattern>[%d{ISO8601}] [%5level] [%thread] [%class] [%method:%line] %msg%n</pattern>
            </encoder>
            <file>${LOG_FILE}.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
                <cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
                <maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
                <totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
                <maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-7}</maxHistory>
            </rollingPolicy>
        </appender>
    </springProfile>

    <springProfile name="dev">
        <appender name="rollingFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <encoder>
                <charset>UTF-8</charset>
                <!-- <pattern>${FILE_LOG_PATTERN}</pattern>  -->
                <pattern>[%d{ISO8601}] [%5level] [%thread] [%class] [%method:%line] %msg%n</pattern>
            </encoder>
            <file>${LOG_FILE}.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
                <cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
                <maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
                <totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
                <maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-7}</maxHistory>
            </rollingPolicy>
        </appender>
    </springProfile>

    <springProfile name="stg">
        <appender name="rollingFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <encoder>
                <charset>UTF-8</charset>
                <!-- <pattern>${FILE_LOG_PATTERN}</pattern>  -->
                <pattern>[%d{ISO8601}] [%5level] [%thread] [%class] [%method:%line] %msg%n</pattern>
            </encoder>
            <file>${LOG_FILE}.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
                <cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
                <maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
                <totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
                <maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-7}</maxHistory>
            </rollingPolicy>
        </appender>
    </springProfile>

    <springProfile name="prod">
        <appender name="rollingFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <encoder>
                <charset>UTF-8</charset>
                <!-- <pattern>${FILE_LOG_PATTERN}</pattern>  -->
                <pattern>[%d{ISO8601}] [%5level] [%thread] [%class] [%method:%line] %msg%n</pattern>
            </encoder>
            <file>${LOG_FILE}.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
                <cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
                <maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
                <totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
                <maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-7}</maxHistory>
            </rollingPolicy>
        </appender>
    </springProfile>

    <root level="INFO">
        <appender-ref ref="consoleAppender" />
        <springProfile name="local">
            <appender-ref ref="rollingFileAppender" />
            <appender-ref ref="consoleAppender" />
        </springProfile>
        <springProfile name="dev">
            <appender-ref ref="rollingFileAppender" />
            <appender-ref ref="consoleAppender" />
        </springProfile>
        <springProfile name="stg">
            <appender-ref ref="rollingFileAppender" />
            <appender-ref ref="consoleAppender" />
        </springProfile>
        <springProfile name="webprod">
            <appender-ref ref="rollingFileAppender" />
            <appender-ref ref="consoleAppender" />
        </springProfile>
        <springProfile name="prod">
            <appender-ref ref="rollingFileAppender" />
            <appender-ref ref="consoleAppender" />
        </springProfile>
    </root>

    <logger name="kr.co.프로젝트명" additivity="true" level="info" />

    <logger name="org.springframework" additivity="true" level="error" />

    <logger name="_org.springframework" additivity="true" level="error" />

    <logger name="io" additivity="true" level="error" />
    <logger name="org" additivity="true" level="error" />
    <logger name="log4jdbc" additivity="true" level="error" />
    <logger name="reactor" additivity="true" level="error" />
    <logger name="springfox" additivity="true" level="error" />

    <logger name="org.apache.kafka" additivity="true" level="info" />

    <logger name="com.zaxxer.hikari" additivity="true" level="error" />
    <logger name="com.zaxxer.hikari.HikariConfig" additivity="true" level="debug" />

    <logger name="org.springframework.transaction" additivity="true" level="debug" />
    <logger name="com.ulisesbocchio" additivity="true" level="error" />

    <logger name="org.apache.ibatis.builder.xml" additivity="true" level="info" />

    <!-- log4jdbc SQL로그 -->
    <logger name="jdbc.connection" additivity="false"/>
    <logger name="jdbc.audit" additivity="false"/>
    <logger name="jdbc.resultset" additivity="false"/>
    <logger name="jdbc.sqlonly" additivity="false"/>

    <!-- SQL로그 -->
    <springProfile name="local">
        <logger name="jdbc.resultsettable" level="DEBUG">
            <appender-ref ref="sqlRollingFileAppender" />
        </logger>
        <logger name="jdbc.sqltiming" level="DEBUG">
            <appender-ref ref="sqlRollingFileAppender" />
        </logger>
    </springProfile>
</configuration>

rollingPolicy fileNamePattern 설정으로 오래된 로그 파일은 프로젝트명.YYYY-MM-DD.순번.gz 파일로 압축되어 저장됩니다.


스프링부트 정보 확인

스프링부트 PID 확인 방법

ps -ef | grep java

실행 중 java 프로세스 목록에서 jar 파일 실행 명령어 좌측에 406806 같은 PID가 확인됩니다.

스프링부트 포트 확인 방법

ss -ltnp | grep java

application.yml 파일 server port와 동일하게 실행되었는지 확인할 수 있습니다.