평소에 인터프리터 언어인 Python과 JavaScript를 사용한 서버 개발을 주로 해왔는데, 최근에 컴파일 언어인 Java를 사용한 서버 개발을 진행할 일이 있었다. 익히 알려져있듯, Spring 앱을 띄우려고 하면 Java는 컴파일 언어이기 때문에 jar 파일을 빌드하는 과정이 따로 필요하다. 이는 주로 인터프리터 언어를 사용해왔던 필자로서는 따로 컴파일 과정을 거쳐야만 서버에 수정 사항을 반영할 수 있다는 부분에서 크게 아쉬움이 느껴졌다.
위와 같은 언어의 근본적인 차이 때문에 아래와 같은 문제가 Docker 환경에서의 개발 진행 시에 아래와 같은 문제가 있었다.
- 인터프리터 언어인 Python 기반의 FastAPI와 JavaScript 기반의 NestJS는 로컬에서의 소스 코드 변경을 Docker Volume Mount로 간단하게 컨테이너 내부에도 적용함으로써 Auto Reload가 가능함
- 하지만 컴파일 언어인 Java 기반의 Spring Boot는 Docker Volume Mount만으로는 로컬에서의 소스 코드 변경을 트리거로 하여 jar파일 빌드와 앱 재실행이라는 두 개의 작업을 처리하기는 어려움
이런 문제로 인해 Spring Boot에서의 로컬 수정 사항을 자동으로 반영하는데에 어려움이 있었고, 이를 해결하기 위해 아래의 포스팅을 참조하여 작업해보았다.
하지만 정확한 이유는 모르겠지만 위 포스팅 대로 여러 번 시도해보았음에도 불구하고, Docker 컨테이너 내에서 jar 파일의 재빌드가 되지 않는 현상이 지속되었다. 그래서 직접 아래와 같은 방법을 아이디어를 고안하여 Spring Boot의 Auto Reload를 구현하였다.
아이디어
우선 Auto Reload 구현을 위해 필요한 것은 아래의 기능들이다.
- 로컬에서의 소스 코드의 변경이 있을 때마다 Docker 컨테이너 내부의 소스 코드를 변경
- Docker 컨테이너 내부의 소스 코드가 변경되면, 이를 재빌드하여 새로운 jar 파일을 생성
- jar 파일이 새로 생성됨을 감지하여, Spring Boot 서버 재실행
위 3개의 작업을 처리할 수 있는 방법을 고안해내면, 결국 Spring Boot 앱의 재실행도 구현할 수 있는 것이다. 그렇기에 각각의 기능을 담당할 수 있는 아래의 작업을 진행하였다.
Docker Volume Mount
로컬에서 소스코드의 수정이 일어나면 docker 컨테이너 내부의 코드도 변경되도록 docker compose 볼륨 마운트 설정
Gradle --continuous
gradle의 continuous build 모드 사용해서 소스 코드 변경 시 자동으로 jar 파일 재빌드
스크립트 파일 작성
jar 파일의 변경 여부를 확인해서 변경되었을 경우에 앱 종료 후 재실행하는 스크립트 파일 작성
위와 같이 설정을 해두게 되면 아래와 같은 과정을 거쳐 Auto Reload가 진행되게 된다.
- 로컬에서 파일이 수정되면 Docker Volume Mount 설정으로 인해 컨테이너 내부의 코드가 바뀜
- 컨테이너 내부 코드의 변경을 감지하면 Gradle --continuous로 인해 jar 파일의 재빌드를 진행
- 스크립트가 jar 파일의 재빌드로 인한 변경을 감지하여 기존 앱을 종료하고 새로운 앱을 재실행 -> Auto Reload 완료
실제 코드
위에서 세운 아이디어를 실제 코드로 아래와 같이 구현하였다. 참고로 필자는 MSA를 위해 docker-compose를 통해 여러 서비스를 띄우는 상태이다. 만약 Spring Boot 하나만 사용할 것이라면, docker compose 사용없이 단순히 docker run -v 옵션으로 마운트를 설정하여도 된다.
- 스크립트 코드(run.sh)
스크립트가 Gradle의 연속 빌드 명령어 실행도 담당하고, jar 파일 변경도 감지하는 두 가지의 중요한 역할을 담당하다보니 스크립트가 조금 긴 편이다. 그래서 주석을 자세하게 달아두었다.
#!/bin/bash
# 애플리케이션 JAR 파일 경로 설정
APP_JAR="/app/build/libs/order-service-0.0.1-SNAPSHOT.jar"
LAST_MODIFIED="" # JAR 파일의 마지막 수정 시간
APP_PID="" # 실행 중인 애플리케이션의 PID
BUILD_PID="" # Gradle 빌드 프로세스의 PID
# 로그 출력 함수
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] - $1"
}
# 애플리케이션 시작 함수
start_app() {
log "Starting application..."
if [ -f "$APP_JAR" ]; then
# JAR 파일이 존재하면 실행
java -jar $APP_JAR & # JAR 파일 실행
APP_PID=$! # $!는 이전 명령의 PID, 즉 실행된 프로세스의 PID를 저장
log "Application started with PID $APP_PID."
else # JAR 파일이 없을 경우 로그 출력
log "JAR file not found. Cannot start application."
fi
}
# 애플리케이션 종료 함수
stop_app() {
if [ -n "$APP_PID" ]; then
# 실행된 애플리케이션이 있을 경우 종료
log "Stopping application with PID $APP_PID..."
kill $APP_PID # PID를 사용해 프로세스 종료
# wait 명령을 사용하여 프로세스가 종료될 때까지 대기
# 2>/dev/null 명령을 사용하여 wait 명령의 stderr(2) 출력을 무시
# || 연산자를 사용하여 $APP_PID 명령이 실패하면($APP_PID에 해당하는 프로세스가 이미 종료됨) 로그 출력
wait $APP_PID 2>/dev/null || log "Application process already stopped."
APP_PID="" # PID 초기화
fi
}
# Gradle 연속 빌드 실행 함수
# Gradle의 --continuous 모드를 사용하여 소스 변경 시 자동으로 빌드가 실행되도록 설정
run_continuous_build() {
log "Starting Gradle build --continuous..."
./gradlew build --continuous -x test & # Gradle 빌드 명령 실행 (백그라운드)
BUILD_PID=$! # 빌드 프로세스의 PID 저장
}
# JAR 파일 변경 감지 및 애플리케이션 재시작 함수
monitor_changes() {
log "Monitoring JAR file changes..."
while true; do
# JAR 파일의 마지막 수정 시간 확인
# stat -c %Y $APP_JAR: stat 명령을 사용하여 $APP_JAR 파일의 마지막 수정 시간를 확인하고 %Y 옵션으로 Unix 타임스탬프를 가져옴
# stat 명령이 실패하면(파일이 없거나 권한이 없는 경우) 0을 출력하도록 설정
CURRENT_MODIFIED=$(stat -c %Y $APP_JAR 2>/dev/null || echo "0")
if [ "$LAST_MODIFIED" != "$CURRENT_MODIFIED" ] && [ "$CURRENT_MODIFIED" != "0" ]; then
# 수정 시간이 변경되면 애플리케이션 재시작
log "Detected changes in $APP_JAR. Restarting application..."
stop_app # 실행 중인 애플리케이션 종료
start_app # 애플리케이션 재시작
LAST_MODIFIED=$CURRENT_MODIFIED # 수정 시간 업데이트
fi
sleep 2 # 변경 감지 간격 (2초)
done
}
# Gradle 연속 빌드 실행
# 파일 변경 시 자동 빌드를 트리거
run_continuous_build
# 초기 설정
log "Initializing script..."
# JAR 파일 확인 및 에러 처리
# 초기 빌드 결과가 없을 경우, JAR 파일 생성될 때까지 대기
# -f 옵션을 사용하여 파일이 존재하는지 확인
if [ ! -f "$APP_JAR" ]; then
log "JAR file not found. Waiting for build..."
while [ ! -f "$APP_JAR" ]; do
sleep 2 # 2초 간격으로 JAR 파일 확인
done
log "JAR file found. Continuing..."
fi
# JAR 파일의 마지막 수정 시간 저장
# JAR 파일의 변경 여부를 확인하기 위해 초기값을 설정
LAST_MODIFIED=$(stat -c %Y $APP_JAR)
# 애플리케이션 실행
# 초기 빌드된 JAR 파일을 실행
start_app
# JAR 파일 변경 감지 시작
# 애플리케이션의 변경 사항 반영을 위해 JAR 파일을 지속적으로 감시
monitor_changes
- Dockerfile
다음으로 Dockerfile에서는 컨테이너 내부에서 사용할 gradle 이미지를 로딩해주고, 컨테이너 구동 시 위의 스크립트를 시작하게 한다.
# OpenJDK 17 Slim 이미지 사용
FROM openjdk:17-jdk-slim
# 애플리케이션 실행 경로 설정
WORKDIR /app
# gradlew 실행을 위한 파일 복사
# Gradle Wrapper 스크립트 및 프로젝트 설정 파일 복사
COPY gradlew gradlew.bat /app/
COPY gradle /app/gradle
COPY build.gradle settings.gradle /app/
COPY src /app/src
# JAR 파일을 실행하기 위한 스크립트 복사
COPY run.sh /app/run.sh
RUN chmod +x /app/run.sh
# run.sh를 실행하도록 설정
CMD ["./run.sh"]
- docker-compose.yml
마지막으로 docker-compose에서는 Spring Boot를 구동 중인 서비스의 컨테이너와 로컬 컴퓨터를 volume mount 시켜주었다.
# Docker Compose 파일의 버전 정의
version: "3.8"
services: # 여러 서비스를 정의
...
order-service: # order-service는 Docker에서 사용할 Spring Boot 서비스의 이름
# order-service의 컨테이너 이미지를 빌드할 때 사용할 Dockerfile 경로를 지정
build: <path-to-dockerfile>
volumes:
- ./services/order-service/src:/app/src
... # 포트번호, 환경변수 등 그 외 서비스 관련 설정
위와 같이 설정을 완료하게 되면 Spring Boot의 Auto Reload가 정상적으로 동작함을 확인할 수 있을 것이다.
'개발 > 개발 팁' 카테고리의 다른 글
Github autoSetupRemote 설정 (0) | 2025.01.17 |
---|---|
SSAFY 11기 전공자 합격 후기 (면접 스터디 X) (0) | 2024.01.13 |
페어 프로그래밍(Pair Programing) (0) | 2023.10.02 |
[부스트캠프 웹・모바일 8기] 네이버 부스트캠프 챌린지 회고 (3) | 2023.08.08 |
AWS 사용하여 배포 시 사용하는 명령어 (0) | 2023.06.21 |