빌드 도구와 의존성 관리 - Gradle/Maven과 Spring Boot 패키징
매일 ./gradlew bootJar나 mvn package를 치고, 만들어진 app.jar를 java -jar로 실행합니다. 그런데 그 사이에 무슨 일이 일어날까요? 의존성에 버전을 안 적었는데 어떻게 빌드가 되고, 하나의 jar 안에 라이브러리가 전부 들어가는 건 어떻게 가능한 걸까요? Series 5(빌드/보안)의 첫 글에서는 빌드 도구가 하는 일, 의존성 관리(BOM), 그리고 실행 가능 jar 패키징을 정리합니다.
TL;DR
- 빌드 도구(Gradle/Maven)는 compile -> test -> package와 의존성 관리를 담당한다. Gradle은 태스크 DAG + DSL, Maven은 phase 생명주기 + XML.
- Spring Boot 플러그인의
bootJar(Gradle) / repackage(Maven)는 앱 + 모든 의존성을 담은 실행 가능 fat jar를 만들어java -jar로 실행하게 한다.- 의존성에 버전을 안 적어도 되는 이유는
spring-boot-dependenciesBOM이 호환되는 버전 집합으로 맞춰 주기 때문이다.
1. 빌드 도구가 하는 일
빌드 도구의 역할은 크게 둘입니다. 하나는 빌드 단계 실행(소스 컴파일, 테스트, 패키징)이고, 다른 하나는 의존성 관리(필요한 라이브러리와 그 전이 의존성을 가져와 버전을 맞추는 것)입니다.
Gradle과 Maven은 접근이 다릅니다.
| 항목 | Maven | Gradle |
|---|---|---|
| 설정 | pom.xml (XML, 선언적) | build.gradle(.kts) (Groovy/Kotlin DSL) |
| 실행 모델 | 고정된 phase 생명주기 | 태스크 그래프(DAG), 증분 빌드/캐시 |
| 빌드 명령 | mvn package | ./gradlew build |
어느 쪽이든 “소스 + 의존성 -> 실행 가능한 산출물”을 만드는 목적은 같습니다.
2. 빌드 파이프라인과 실행 가능 jar
빌드의 흐름과 최종 산출물을 정리하면 다음과 같습니다.
fat jar는 애플리케이션과 모든 dependency를 함께 담기 때문에 Spring Boot 전용 classloader와 launcher를 사용합니다.
compile + test -> Spring Boot 플러그인 패키징 -> 앱과 모든 의존성을 담은 실행 가능 jar -> java -jar로 실행.
Spring Boot Gradle 플러그인은 bootJar 태스크로 실행 가능 jar를 만듭니다.
Executable jars can be built using the
bootJartask.
- Spring Boot Gradle Plugin Reference
이 jar는 애플리케이션의 모든 의존성을 포함해 java -jar로 바로 실행됩니다.
The plugin can create executable archives (jar files and war files) that contain all of an application’s dependencies and can then be run with
java -jar.
- Spring Boot Gradle Plugin Reference
내부 레이아웃은 Series 1의 fat jar 편에서 본 것과 같습니다.
By default, the
bootJartask builds an archive that contains the application’s classes and dependencies inBOOT-INF/classesandBOOT-INF/librespectively.
- Spring Boot Gradle Plugin Reference
즉 BOOT-INF/classes(내 코드) + BOOT-INF/lib(의존성 jar들) 구조이고, 이걸 JarLauncher가 전용 클래스로더로 실행합니다. 빌드의 산출물이 곧 Series 1에서 본 실행 구조입니다.
3. 의존성 관리 - 버전을 안 적어도 되는 이유
Spring Boot 프로젝트의 build.gradle을 보면 의존성에 버전이 없습니다.
1
2
3
4
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
이게 가능한 이유는 dependency management(BOM) 때문입니다.
BOM이 제공하는 검증된 버전 집합이 충돌과 불일치를 줄이며, 개별 version override는 이 보장 범위를 벗어날 수 있습니다.
버전을 생략해 선언하면, spring-boot-dependencies BOM이 호환되는 버전 집합으로 채워 직접/전이 의존성을 정렬한다.
When you apply the
io.spring.dependency-managementplugin, Spring Boot’s plugin will automatically import thespring-boot-dependenciesbom from the version of Spring Boot that you are using. … it allows you to omit version numbers when declaring dependencies that are managed in the bom.
- Spring Boot Gradle Plugin Reference
spring-boot-dependencies BOM은 수많은 라이브러리의 검증된 호환 버전 집합을 정의합니다. 그래서 버전을 직접 안 적어도 BOM이 채워 주고, 라이브러리 간 버전 충돌이 줄어듭니다. starter(예: spring-boot-starter-web)는 이 위에서 “이 기능에 필요한 의존성 묶음”을 한 번에 가져오는 역할을 합니다.
4. 함정과 팁
- 버전 강제 override 주의. BOM이 관리하는 라이브러리의 버전을 임의로 올리면, BOM이 보장하던 호환 집합을 벗어나 미묘한 충돌이 날 수 있습니다. 꼭 필요할 때만, 영향 범위를 확인하고 바꿉니다.
- 전이 의존성 충돌 확인. 같은 라이브러리를 서로 다른 버전이 끌고 오면 충돌합니다.
./gradlew dependencyInsight --dependency <name>또는mvn dependency:tree로 실제 해석된 버전을 확인합니다. - 무엇이 왜 들어왔는지 본다. fat jar의
BOOT-INF/lib가 비대해지면 의존성 트리를 점검해 불필요한 것을 줄입니다.
fat jar는 Spring Boot가 실행할 classpath를 고정한 산출물이고, BOM은 그 안의 dependency version을 함께 관리합니다. Spring Security의 filter chain은 Spring Security 기본에서, 버전 업그레이드 뒤 runtime 동작 변화는 JVM Spring capstone에서 이어집니다.
BOM과 version pinning은 재현 가능한 build의 기반이며, dependency tree는 SBOM과 취약점 scan이 실제로 어떤 library를 다루는지 보여 주는 출발점입니다.
5. 참고 자료
- Spring Boot Gradle Plugin - Packaging Executable Archives (
bootJar/ fat jar / BOOT-INF): https://docs.spring.io/spring-boot/gradle-plugin/packaging.html - Spring Boot Gradle Plugin - Managing Dependencies (dependency management / BOM): https://docs.spring.io/spring-boot/gradle-plugin/managing-dependencies.html
- Spring Boot Maven Plugin (repackage / 의존성 관리): https://docs.spring.io/spring-boot/maven-plugin/
궁금하신 점이나 추가해야 할 부분은 댓글이나 아래의 링크를 통해 문의해주세요.
Written with KKamJi
