pom.xml 최소 구성Maven 은 모든 라이브러리를 groupId(조직), artifactId(라이브러리 이름), version(버전) 세 값으로 식별합니다. 이 세 값을 합쳐 좌표라고 부릅니다. pom.xml 은 이 좌표들과 빌드 방법을 적는 파일입니다.
Spring Boot 프로젝트는 보통 spring-boot-starter-parent 를 부모로 상속해 버전 관리 대부분을 부모에 맡깁니다. properties 에 java.version 을 적으면 컴파일러 버전이 맞춰지고, dependencies 에는 라이브러리 좌표를, build/plugins 에는 컴파일·패키징 방식을 커스터마이즈하는 플러그인을 적습니다.
같은 라이브러리라도 언제 필요한지에 따라 범위가 다릅니다. 범위를 잘못 잡으면 실행 시 ClassNotFoundException 이 나거나, 반대로 배포 jar 가 불필요하게 커집니다.
| scope | 컴파일 | 실행(런타임) | 배포 포함 | 예 |
|---|---|---|---|---|
| compile(기본값) | O | O | O | spring-boot-starter-web |
| provided | O | X(컨테이너 제공) | X | servlet-api |
| runtime | X | O | O | mysql-connector-j |
| test | 테스트만 | 테스트만 | X | junit-jupiter |
provided 는 WAS 가 이미 클래스를 제공해서 배포 jar 에는 넣지 않는 경우입니다. runtime 은 코드에서 직접 참조하지 않고 인터페이스로만 쓰는 JDBC 드라이버 같은 라이브러리에 씁니다.
A 를 추가하면 A 가 쓰는 B, C 까지 자동으로 딸려 옵니다. 이것이 전이 의존성입니다. 문제는 서로 다른 라이브러리가 같은 B 의 다른 버전을 요구할 때입니다.
Maven 은 기본적으로 "POM 트리에서 더 가까운 버전이 승리"하는 규칙을 씁니다. 같은 깊이면 먼저 선언된 쪽이 이깁니다. mvn dependency:tree 로 실제 어떤 버전이 선택됐는지, 어디서 충돌이 나는지 확인합니다.
mvn dependency:tree
# [INFO] +- com.example:lib-a:jar:1.0:compile
# [INFO] | \- (commons-lang3:3.12 -> 3.14로 승격, lib-b 요구)원치 않는 버전이 딸려 오면 두 가지 방법으로 고칩니다. <exclusions> 로 특정 전이 의존성을 빼거나, dependencyManagement(또는 BOM: Bill of Materials) 로 버전을 프로젝트 전체에 고정합니다. spring-boot-starter-parent 자체가 거대한 BOM 역할을 합니다.
이 회사는 Maven 을 표준으로 쓰지만, 오픈소스 예제는 Gradle 로 배포되는 경우가 많아 대응 개념을 알아 둘 필요가 있습니다. build.gradle.kts 는 이 레슨 폴더에 문법 참고용으로만 넣었고 실행 검증은 하지 않습니다.
| Maven scope | Gradle configuration |
|---|---|
| compile | implementation |
| provided | compileOnly |
| runtime | runtimeOnly |
| test | testImplementation |
Gradle 에서 전이 의존성 트리는 gradle dependencies 로 봅니다. mvn dependency:tree 와 목적은 같습니다.
~/.m2/repository 구조Maven 은 한 번 받은 jar 를 홈 폴더의 ~/.m2/repository 에 캐시합니다. 경로는 좌표를 그대로 폴더로 풀어낸 구조입니다.
~/.m2/repository/
org/springframework/boot/
spring-boot-starter-web/
3.3.0/
spring-boot-starter-web-3.3.0.jar
spring-boot-starter-web-3.3.0.pom
spring-boot-starter-web-3.3.0.jar.sha1groupId 의 점(.)이 폴더 구분자(/)로 바뀌고, 그 아래 artifactId/version/ 순서로 폴더가 생깁니다. jar·pom 파일마다 .sha1 체크섬 파일이 같이 있어 무결성을 확인할 수 있습니다. 이 폴더 전체가 폐쇄망 반입의 핵심 대상입니다.
(a) ~/.m2/repository 통째로 반입. 인터넷 PC에서 mvn dependency:go-offline 을 실행하면 pom.xml 이 필요로 하는 의존성과 빌드 플러그인까지 로컬 저장소에 미리 받아 둡니다. 이 폴더를 tar 로 묶어 폐쇄망 서버로 옮기고, settings.xml 에서 <offline>true</offline> 와 <localRepository> 경로를 지정합니다.
이 방식은 준비가 간단하지만, 프로젝트를 새로 추가할 때마다 인터넷 PC에서 다시 받아 전체를 재반입해야 하는 단점이 있습니다. 사내 표준 저장소가 없는 소규모 팀에 적합합니다.
(b) 사내 Nexus·Artifactory 미러. 사내에 프록시 저장소를 두고, 폐쇄망 서버는 이 미러만 바라보게 settings.xml 의 <mirror> 를 설정합니다. 신규 라이브러리는 보안팀 심사를 거쳐 미러의 허용 목록에 등록하는 절차를 밟습니다.
이 방식은 초기 구축 비용이 있지만, 여러 팀·여러 프로젝트가 반복 반입 없이 공유할 수 있어 실무에서 가장 널리 쓰입니다. settings-offline.xml 예시 파일에 mirror 설정을 주석으로 넣어 두었습니다.
(c) jar 개별 반입. 특정 jar 하나만 급하게 필요할 때는 mvn install:install-file 로 로컬 저장소에 수동 등록합니다. <scope>system</scope> 로 프로젝트 폴더 안 jar 를 직접 가리키는 방법도 있지만, 저장소 관리가 안 되고 팀원마다 경로가 달라질 수 있어 권장하지 않습니다.
mvn install:install-file \
-Dfile=mybatis-3.5.19.jar \
-DgroupId=org.mybatis -DartifactId=mybatis \
-Dversion=3.5.19 -Dpackaging=jarjar 를 옮기는 과정에서 파일이 깨지거나, 의도치 않은 버전이 섞여 들어갈 수 있습니다. 반입 전후로 sha256sum 값을 대조해 무결성을 확인합니다. 라이선스 고지문 확인과 알려진 취약점(CVE) 점검도 반입 심사에 포함합니다. CVE 점검 방법은 실무 확장 20 웹 보안 레슨을 참조하세요.
이 레슨의 import-jars.sh 는 .sha256 사이드카 파일이 있는 jar 만 대조하고, 없는 jar 는 "미검증"으로만 표시합니다. 실제 반입 심사에서는 미검증 jar 를 그대로 승인하면 안 됩니다.
같은 pom.xml 로 언제 빌드해도 같은 결과가 나와야 합니다. 이를 위해 버전은 항상 숫자로 고정하고, LATEST·RELEASE·SNAPSHOT 버전은 운영 빌드에 쓰지 않습니다. 시점에 따라 실제로 받아지는 jar 가 달라지기 때문입니다.
Maven Wrapper(mvnw, mvnw.cmd)는 팀원마다 다른 Maven 버전을 쓰는 문제를 없애 줍니다. 폐쇄망에서는 Wrapper 가 내려받는 Maven 배포판 자체도 미리 반입해야 동작합니다.
일반 mvn package 는 프로젝트 코드만 담은 jar 를 만듭니다. 이 jar 는 의존성이 클래스패스에 따로 있어야 실행됩니다. Spring Boot 는 spring-boot-maven-plugin 의 repackage 목표로 의존성까지 하나에 담은 실행 가능한 jar(fat jar, uber jar)를 만듭니다.
fat jar 내부에는 BOOT-INF/classes(프로젝트 클래스)와 BOOT-INF/lib(의존성 jar 전체)가 들어 있습니다. 매니페스트의 Main-Class 는 Spring Boot 런처를 가리키고, 실제 애플리케이션의 진입점은 Start-Class 에 별도로 기록됩니다. -classifier exec 옵션을 주면 일반 jar 와 fat jar 를 파일명으로 구분할 수 있습니다.
빌드 서버에서는 보통 mvn -o -B -q -DskipTests package 형태로 실행합니다. -o 는 오프라인, -B 는 배치 모드(진행바 등 대화형 출력 억제), -q 는 최소 로그, -DskipTests 는 배포 파이프라인에서 테스트를 따로 돌릴 때 씁니다. 종료 코드가 0 이 아니면 후속 배포 단계로 넘어가지 않게 스크립트에서 체크합니다.
산출물 파일명에 버전과 git 커밋 해시를 넣어 두면 배포 서버에서 어떤 소스로 만든 jar 인지 바로 알 수 있습니다. 파일명 규칙과 배포 스크립트는 리눅스 04 배포 레슨을 참조하세요.
자주 보는 오류 메시지를 표로 정리합니다.
| 오류 메시지(일부) | 원인 | 조치 |
|---|---|---|
Could not resolve dependencies |
jar 를 저장소에서 못 찾음 | 반입 누락, 좌표 오탈자 확인 |
Non-resolvable parent POM |
부모 POM 을 못 받음 | starter-parent 도 반입 대상 |
plugin ... not available in offline mode |
플러그인 자체가 캐시에 없음 | 플러그인도 go-offline 대상 |
인코딩 경고(불특정 문자 인코딩) |
sourceEncoding 미설정 | project.build.sourceEncoding 지정 |
두 번째와 세 번째 오류는 초심자가 자주 놓칩니다. dependency:go-offline 은 dependencies 뿐 아니라 부모 POM 과 빌드에 쓰는 플러그인까지 받아 두므로, 반입 전에 반드시 실행해야 합니다.
Gradle 도 --offline 옵션으로 네트워크를 막고 캐시만 쓸 수 있습니다. 캐시 위치는 ~/.gradle/caches 이며 Maven 의 ~/.m2/repository 와 폴더 구조가 다릅니다.
Gradle 캐시는 메타데이터와 잠금 파일이 Gradle 버전에 민감합니다. 폐쇄망에 반입할 때 개발 PC와 빌드 서버의 Gradle 버전이 다르면 캐시를 인식하지 못하는 경우가 있어, 버전을 정확히 맞추거나 Gradle Wrapper 배포판 자체를 함께 반입해야 합니다.
이 사이트의 실무 확장 예제는 java-src/lib 폴더에 jar 를 미리 넣어 두고 -cp "..\..\lib\*;." 로 클래스패스에 잡습니다. 예제 하나만 실행하면 되므로 전이 의존성·버전 충돌·표준 폴더 구조가 필요 없어 이 방식이 더 간단합니다.
실제 프로젝트는 모듈이 여러 개고 팀원이 여러 명이라 이 방식이 곧바로 한계에 부딪힙니다. java-src/lib 는 학습용 지름길이고, 실무에서는 Maven·Gradle 이 표준이라는 점을 구분해서 이해하는 것이 이 레슨의 핵심입니다.