Spring Boot 개발기록 #1 - 시작, 배포
회사에서 Spring을 사용할 일이 생겨 기본적인 흐름에 익숙해지기로 했다. 오늘은 간단하게 Spring 프로젝트를 시작하고, 기본 설정을 구성한 뒤 서버에 배포하는 과정을 두세 번 반복하는 것을 목표로 삼았다.
프로젝트 시작 - Spring Initializr
Spring 프로젝트는 Spring Initializr로 시작했다. 웹에서 클릭 몇 번으로 프로젝트 기본 구조를 생성해준다.
선택한 옵션:
- Project: Gradle - Groovy
- Language: Java
- Spring Boot: 3.5.6 (기본 선택 버전)
- Java: 17 (LTS)
- Dependencies: Spring Web, Spring Data JPA, PostgreSQL Driver 등등
엄청 오랜만에 만져보는 사이트라 설정에 따라 프로젝트에 뭐가 달라지는 건지 이것저것 만지작거렸다.
Java 17?
예전에 썼던 건 Java 8, 11 등이었던 것 같은데 17 버전인 게 지금 제일 안정된 버전인지 궁금했다. 찾아보니 Java 17은 LTS(Long Term Support) 버전이고, Java 8 이후로 무료 지원이 유지되고 있었다. Oracle JDK는 Java 11부터 유료였다가, Java 17부터 다시 무료로 전환됐다고 한다.
그리고 Java를 받을 수 있는 곳도 여러 군데였다. OpenJDK에서도 받을 수 있고, Eclipse Temurin(구 AdoptOpenJDK)에서도 받을 수 있던데 뭔 차이인지 궁금했다. 알고 보니 소스 코드는 모두 동일한 OpenJDK인데, 누가 빌드하고 배포하는지의 차이였다. Eclipse Temurin은 검증되고 관리되는 빌드를 제공해서, 일반 개발자에게는 이게 더 편하다고 한다.
Gradle vs Maven - 빌드 도구의 선택
예전에 Spring을 진행했을 때는 Maven 기반 프로젝트를 만들었던 것으로 어렴풋이 기억하는데, 요즘은 공식 문서에서부터 Gradle을 기본으로 추천하고 있다. 회사에서 참여할 프로젝트 역시 Gradle로 구성되어 있다.
스스로는 npm, yarn 같은 의존성 관리 툴로 인식했는데, Gradle이 정확히 어떤 역할을 하는지부터 찾아봤다.
CLI에서 프로젝트 실행하기
IntelliJ가 GUI 환경에서 제공하는 간편한 실행 기능들을 Node.js 환경처럼 CLI에서는 어떻게 수행하는지 궁금했다.
# npm 프로젝트
npm install # 의존성 설치
npm start # 실행
# Spring Boot (Gradle)
.\gradlew.bat bootRun # 의존성 자동 설치 + 실행
이전부터 궁금했던 건데, IntelliJ를 사용하면 npm처럼 프로젝트 install을 자동으로 해주는 줄 알았다. IntelliJ 툴이 그 부분을 처리해주는 건가 싶었는데, 찾아보니 IntelliJ가 프로젝트를 열 때 Gradle Wrapper를 자동으로 실행시켜주고, Gradle Wrapper가 실제 의존성을 다운로드한다고 한다. npm과 달리 별도의 install 명령어 없이 실행 시 자동으로 처리된다는 것이다. 게다가 node_modules/처럼 프로젝트별로 설치되는 게 아니라 C:\Users\[username]\.gradle\caches\에 전역 캐시로 관리되어 모든 Gradle 프로젝트가 공유한다고 한다.
아직 이 부분은 직접 팩트체크를 해보지 않았는데, 프로젝트를 진행하면서 실제로 어떻게 동작하는지 살펴봐야겠다.
JAVA_HOME 환경변수 설정
IntelliJ의 도움 없이 CLI로 실행하려니 JAVA_HOME 환경변수 설정 에러가 등장했다. 그런데 이상한 게, 내 로컬 환경에는 아직 Java를 설치하지 않았는데도 IntelliJ에서는 프로젝트를 구동시킬 수 있었다.
알고 보니 IntelliJ는 자체 JDK를 번들로 포함하고 있어서, 시스템에 Java가 없어도 IDE 내에서는 실행할 수 있었다. 하지만 CLI에서 실행하려면 시스템 레벨의 Java 설치가 필요했다.
예전에 Java를 설치할 때 번거로운 작업들이 많았던 것 같은데, 이번엔 Eclipse Temurin을 사용해서 편하게 설치했다. 설치 후 java -version 명령은 정상적으로 실행됐지만, JAVA_HOME 환경변수는 자동으로 등록되지 않았다. Gradle에서 JAVA_HOME을 사용하는 것 같아서, 이 부분은 따로 환경변수를 수동으로 설정해줬다.
# 설치 후 확인
java -version # 정상 작동
echo %JAVA_HOME% # 설정되지 않음
# 수동 설정 필요
# Win + R → sysdm.cpl → 환경 변수
# 시스템 변수 → 새로 만들기
# 변수 이름: JAVA_HOME
# 변수 값: C:\Program Files\Eclipse Adoptium\jdk-17.x.x.x-hotspot
환경변수 관리 - Spring Profile
다음으로 궁금했던 건 Spring에서 애플리케이션 환경변수를 배포 환경에 따라 어떻게 구분하는지였다. Node.js의 .env 파일처럼 관리하는 방법이 있을 것 같았는데, 찾아보니 Spring에서는 Profile이라는 개념으로 환경별 설정을 관리한다고 한다.
파일 구조
src/main/resources/
├── application.properties # 공통 설정 (Git 포함)
├── application-local.properties # 로컬 개발용 (Git 제외)
└── application-prod.properties # 운영용 (Git 제외)
application.properties (공통 설정):
spring.application.name=spring-progressive-demo
# 기본 프로필 (로컬 개발용)
spring.profiles.active=local
# JPA 공통 설정
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
application-local.properties (로컬 개발용):
spring.datasource.url=jdbc:postgresql://localhost:5432/spring_progressive_demo
spring.datasource.username=postgres
spring.datasource.password=local_password
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=create-drop
application-prod.properties (운영용):
spring.datasource.url=jdbc:postgresql://prod-db-host:5432/spring_progressive_demo
spring.datasource.username=prod_user
spring.datasource.password=prod_password
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
.gitignore 설정
중요한 건 .gitignore 설정이다. Spring Boot는 이미 application-*.properties를 기본으로 무시하도록 설정되어 있어서, 민감한 정보가 Git에 포함되지 않는다.
# .gitignore (이미 포함되어 있음)
application-*.properties
Profile 사용 방법
실행 시 프로필을 지정하는 방법도 여러 가지다:
# 1. application.properties에 설정 (기본값)
spring.profiles.active=local
# 2. 실행 시 지정
java -jar app.jar --spring.profiles.active=prod
# 3. 환경변수로
export SPRING_PROFILES_ACTIVE=prod
java -jar app.jar
로컬 개발 시에는 application.properties에 spring.profiles.active=local 설정이 되어 있어서 자동으로 application-local.properties를 사용한다.
Swagger & Scalar - API 문서화
API를 제공하는 백엔드 애플리케이션이라면 대부분 가져가는 Swagger 설정까지 진행해보고 배포를 하려고 했다.
일단, 나는 Swagger를 붙이는 프로젝트면 항상 Scalar UI를 함께 사용하는데, Swagger UI보다 훨씬 모던하고 현대적이라 개인적으로 선호한다. 물론 필수는 아니지만 말이다.
SpringDoc OpenAPI 설치
Swagger 설정부터는 귀찮아져서 Claude에게 시켰다. Spring Boot 3.x에서는 기존 springfox가 아닌 SpringDoc OpenAPI를 사용해야 한다고 하면서, 다음과 같이 설치하라고 했다:
build.gradle:
dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
}
호환성 이슈
Swagger에서 DTO를 테스트하던 중, 원하는 대로 에러 메시지를 뱉지 않아서 예외 처리를 위한 GlobalExceptionHandler를 만들었다. @RestControllerAdvice를 사용해서 예외 클래스를 작성하고 다시 컴파일하니까 에러가 발생했다:
java.lang.NoSuchMethodError: 'void org.springframework.web.method.ControllerAdviceBean.<init>(java.lang.Object)'
Stack Overflow를 찾아보니, SpringDoc 2.3.0 버전이 현재 설치된 Spring Boot 3.5.6과 호환이 안 되는 문제였다.
Claude는 "Spring Boot 버전을 3.4.1로 다운그레이드해야 한다"고 했지만, 짜치는 느낌이 들어서 찾아보니 SpringDoc의 최신 버전이 2.8.8(2024년 11월 출시)이었다. AI가 알고 있던 2.3.0은 구버전이었던 것이다.
최신 버전으로 업데이트:
// Before
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
// After
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8'
업데이트 후 문제가 해결되었다.
공식 문서에는 Spring Boot 3.4.x까지만 지원한다고 명시되어 있었지만, 실제로는 3.5.6에서도 잘 작동했다. 이전에 비슷한 경험들을 몇번 해서 이제는 안당해준다.
Scalar UI 설정
SpringDoc을 설치하면 기본적으로 Swagger UI(/swagger-ui.html)가 활성화된다. 여기에 Scalar UI로 덮어씌웠다.
src/main/resources/static/api-docs.html:
<!doctype html>
<html>
<head>
<title>API Documentation</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<script
id="api-reference"
data-url="/v3/api-docs"
data-configuration='{
"theme": "bluePlanet",
"showSidebar": true,
"darkMode": true,
"layout": "modern",
"defaultOpenAllTags": true,
"searchHotKey": "k"
}'
></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>
JAR 빌드와 배포
개발 중에는 gradlew.bat bootRun으로 실행하지만, 배포할 때는 JAR 파일로 패키징한다.
빌드 프로세스
# 프로젝트 빌드
.\gradlew.bat build
# 생성된 파일 확인
dir build\libs
생성되는 파일:
build\libs\
├── spring-progressive-demo-0.0.1-SNAPSHOT.jar # 실행 가능 (Fat JAR)
└── spring-progressive-demo-0.0.1-SNAPSHOT-plain.jar # 라이브러리용
헷갈렸던 건 두 개의 JAR 파일이 생성된다는 점이다:
- Executable JAR (
SNAPSHOT.jar): 모든 의존성과 내장 Tomcat을 포함한 Fat JAR. 독립적으로 실행 가능하며 배포 시 사용 ✅ - Plain JAR (
SNAPSHOT-plain.jar): 프로젝트 코드만 담긴 라이브러리용. 실행 불가 ❌
JAR 실행
# 로컬 프로필로 실행
java -jar build\libs\spring-progressive-demo-0.0.1-SNAPSHOT.jar
# 운영 프로필로 실행
java -jar build\libs\spring-progressive-demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
Docker 배포
스프링에서 Docker로 빌드하고 배포하는 걸로 마무리하기로 했다.
Dockerfile
# Stage 1: Build (Gradle로 JAR 생성)
FROM gradle:8.5-jdk17 AS build
WORKDIR /app
# Gradle 의존성 캐싱
COPY build.gradle settings.gradle ./
RUN gradle dependencies --no-daemon
# 소스 복사 및 빌드
COPY . .
RUN gradle build --no-daemon -x test
# Stage 2: Runtime (JRE로 실행)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# JAR 파일 복사
COPY --from=build /app/build/libs/*.jar app.jar
# 실행
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
docker-compose.yml
내 서버에 올릴 거라서 기존의 프로젝트랑 비슷하게 구색을 맞췄다.
version: '3.8'
services:
spring-progressive-demo:
image: spring-progressive-demo:${APP_VERSION:-latest}
container_name: spring-progressive-demo
ports:
- "8005:8080"
volumes:
- ./src/main/resources/application-prod.properties:/.....
environment:
- SPRING_PROFILES_ACTIVE=prod
restart: unless-stopped
networks:
- my-network
networks:
my-network:
external: true
GitHub Actions CI/CD
GitHub Actions로 대충 CI/CD를 구성하고 돌렸더니 배포까지 성공했다.
# release 브랜치 푸시
git checkout -b release/1.0.0
git push origin release/1.0.0
# GitHub Actions 자동 실행 → 홈서버 배포 완료
마무리
Spring Boot 프로젝트를 시작부터 배포까지 한 번 경험해보니, 나쁘지 않은듯..? 하다. 몇번 더 찍먹해보고 마음속에 울림이 생길 때 관련 내용들을 정리해봐야겠다.