배포 요청이 왔습니다. 개발팀이 "배포 해주세요"라며 GitLab 저장소 링크만 던져줬습니다. 저장소를 받아보니 Maven 프로젝트와 React 프론트엔드가 섞여 있습니다. WAR 파일은 어떻게 만드는지, 운영 프로파일로 빌드해야 하는지, npm build는 뭘로 하는지 — 처음 보는 프로젝트를 빌드해야 하는 상황입니다.
이 모듈은 Java 빌드 도구(Maven/Gradle)와 프론트엔드 빌드 도구(npm/pnpm)의 실무 사용법을 다룹니다. 의존성 충돌 해결과 산출물 확인까지 포함합니다.
- 1Maven 라이프사이클(clean/compile/test/package)과 운영 빌드 명령을 실행할 수 있다
- 2Gradle ./gradlew clean build 로 빌드하고 산출물 위치를 확인할 수 있다
- 3npm ci와 npm install의 차이를 설명하고 CI 환경에서 올바른 명령을 선택할 수 있다
- 4mvn dependency:tree와 ./gradlew dependencies로 의존성 충돌을 확인할 수 있다
- 5빌드 실패 원인을 컴파일 오류/테스트 실패/의존성 오류로 분류할 수 있다
Maven — Java 빌드 표준
Maven 라이프사이클과 pom.xml
Maven은 Java 프로젝트 빌드의 표준입니다. 빌드 단계(라이프사이클)가 고정되어 있어 처음 보는 프로젝트도 패턴이 동일합니다. mvn package는 항상 target/ 디렉터리에 WAR/JAR를 만듭니다.
확대
긴급 배포 요청이 왔습니다. 저장소를 클론했는데 mvn package를 실행하면 테스트가 30분씩 걸리고, 운영 프로파일로 빌드했는지 확인할 방법도 모릅니다. 빌드 로그 어딘가에 에러가 있는데 BUILD FAILURE 한 줄만 보입니다. 빌드 도구를 모르면 처음 보는 프로젝트를 배포할 때 판단 근거가 없습니다. Maven 라이프사이클을 이해하면 어느 단계에서 실패했는지, 테스트를 건너뛰려면 어떤 옵션을 쓰는지, 산출물이 어디 생기는지를 즉시 파악할 수 있습니다.
Maven 라이프사이클 (순서대로 실행):
clean → validate → compile → test → package → install → deploy
| 단계 | 설명 |
|---|---|
clean | target/ 디렉터리 삭제 (이전 빌드 산출물 제거) |
compile | src/main/java 소스 컴파일 |
test | src/test/java 테스트 실행 |
package | WAR 또는 JAR 생성 → target/ 에 저장 |
install | 로컬 Maven 저장소(~/.m2/repository)에 설치 |
# 운영 빌드 표준 명령 (테스트는 CI에서 별도 실행한다고 가정)
mvn clean package -DskipTests -Pprod
# 옵션 해설
# clean — 이전 빌드 산출물 제거 (필수)
# package — compile → test(skip) → package 단계까지
# -DskipTests — 테스트 실행 건너뜀
# -Pprod — prod 빌드 프로파일 활성화 (pom.xml에 정의된 경우)
# 빌드 산출물 확인
ls -lh target/*.war
ls -lh target/*.jar
pom.xml 핵심 구조:
<!-- pom.xml -->
<project>
<groupId>com.example</groupId>
<artifactId>myapp</artifactId>
<version>1.2.0</version>
<packaging>war</packaging> <!-- war 또는 jar -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies>
<!-- 빌드 프로파일 -->
<profiles>
<profile>
<id>prod</id>
<properties>
<spring.profiles.active>prod</spring.profiles.active>
</properties>
</profile>
</profiles>
</project>
Gradle — 빠른 빌드와 유연한 DSL
Gradle 빌드와 ./gradlew
Gradle은 Maven보다 빌드 속도가 빠릅니다. 변경된 파일만 재빌드하는 증분 빌드가 기본 동작합니다. ./gradlew(Gradle Wrapper)를 쓰면 Gradle을 별도로 설치하지 않아도 됩니다.
# Gradle Wrapper 빌드 (권장 — 프로젝트 내 버전 사용)
./gradlew clean build
# 테스트 건너뛰기
./gradlew clean build -x test
# 특정 태스크만 실행
./gradlew bootJar # Spring Boot JAR 생성
./gradlew bootWar # Spring Boot WAR 생성
# 산출물 위치 확인
ls -lh build/libs/
// build.gradle (기본 구조)
plugins {
id 'org.springframework.boot' version '3.1.0'
id 'java'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
// 프로파일별 빌드 설정
bootJar {
archiveFileName = "myapp-${version}.jar"
}
npm/pnpm — 프론트엔드 빌드
npm 빌드와 산출물 관리
React/Vue 같은 프론트엔드 프로젝트는 npm으로 빌드합니다. 빌드 결과물은 dist/ 또는 build/ 디렉터리에 정적 파일로 생성됩니다. 이 파일들을 Nginx로 서빙합니다.
# 의존성 설치 (CI/CD 환경에서는 반드시 ci 사용)
npm ci # package-lock.json 기준, 재현 가능한 설치
npm install # 개발 로컬 환경에서 신규 패키지 추가 시
# 운영 빌드
npm run build
# 또는 커스텀 스크립트가 있으면
npm run build:prod
# 빌드 산출물 확인
ls -lh dist/
# 또는
ls -lh build/
# pnpm 사용 시
pnpm install --frozen-lockfile # CI 환경
pnpm run build
package.json 스크립트 확인:
# 어떤 빌드 스크립트가 있는지 먼저 확인
cat package.json | python3 -m json.tool | grep -A20 '"scripts"'
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"build:prod": "REACT_APP_ENV=production react-scripts build",
"test": "react-scripts test"
}
}
확대
실습
실제 Maven 프로젝트가 있는 디렉터리에서 실행합니다. 빌드 로그가 길게 흐르고 마지막에 BUILD SUCCESS가 나오면 성공입니다.
# 빌드 전 — 이전 산출물 있는지 확인
ls target/ 2>/dev/null || echo "target 없음"
# 운영 빌드
mvn clean package -DskipTests -Pprod
# 빌드 후 산출물 확인
ls -lh target/*.war target/*.jar 2>/dev/null
# 빌드 시간 확인 (출력 마지막 줄)
# [INFO] BUILD SUCCESS
# [INFO] Total time: 42.318 s
[INFO] BUILD SUCCESS
[INFO] Total time: 42.318 s
[INFO] Finished at: 2026-05-30T10:45:22+09:00
mvn clean package -DskipTests -Pprod- mvn 출력 마지막 줄부터 확인 — BUILD SUCCESS면 정상. BUILD FAILURE면 스크롤 업해서 "[ERROR]"로 시작하는 첫 줄을 찾는다. 그 줄이 실제 원인이고 이후 줄은 연쇄 오류
- ls -lh target/*.war로 산출물 크기 확인 — 1KB 미만이면 빈 WAR(빌드 설정 오류), 정상 WAR는 최소 수십 MB. 크기가 이전 빌드 대비 50% 이상 줄었으면 리소스 누락 가능성
- -Pprod 빌드 후 로그에 "The following profiles are active: prod"가 없으면 프로파일 미적용 — 빌드된 WAR가 개발 설정으로 패키징된 것. 파일명에 버전 번호가 있어야 배포 추적 가능
의존성 충돌은 빌드가 성공해도 런타임에 ClassNotFoundException이나 NoSuchMethodError를 일으킬 수 있습니다. 빌드 성공 후에도 충돌 여부를 확인하는 것이 좋습니다.
# Maven 의존성 트리 전체 출력
mvn dependency:tree
# 충돌/제외된 의존성만 필터링
mvn dependency:tree | grep -E "omitted for conflict|omitted for duplicate"
# Gradle 의존성 확인
./gradlew dependencies | grep -E "conflict|FAILED"
# 특정 라이브러리 버전이 어디서 왔는지 확인
mvn dependency:tree -Dincludes=org.slf4j:slf4j-api
[INFO] +- org.springframework.boot:spring-boot-starter-logging:jar:3.1.0:compile
[INFO] | \- org.slf4j:slf4j-api:jar:2.0.7:compile
[INFO] \- com.example:legacy-module:jar:1.0.0:compile
[INFO] \- (org.slf4j:slf4j-api:jar:1.7.36:compile - omitted for conflict with 2.0.7)
mvn dependency:tree | grep -E 'CONFLICT|WARNING|omitted'- "omitted for conflict" 줄을 먼저 찾는다 — 없으면 의존성 충돌 없음. 있으면 괄호 안의 두 버전 번호를 비교해 상위 버전이 채택됐는지 확인(Maven은 가장 가까운 경로 우선)
- 충돌 라이브러리가 slf4j, jackson, spring-core 같은 핵심 라이브러리인 경우 — 1.x vs 2.x처럼 major 버전 차이가 있으면 런타임 NoSuchMethodError 발생 가능. pom.xml exclusion 즉시 적용 검토
- FAILED 항목 없고 충돌도 없는데 런타임에 ClassNotFoundException이 나면 — 빌드 타임엔 compile 스코프로 포함됐지만 WAR에서 provided 스코프가 빠진 경우. mvn dependency:analyze로 미사용/누락 의존성 확인
트러블슈팅
원인: 회사 내부 라이브러리를 사내 Nexus(내부 Maven 저장소)에서 받아야 하는데, Nexus 주소 설정이 없거나 네트워크가 차단됐습니다. 폐쇄망 서버에서 자주 발생합니다.
# 1. Maven 저장소 설정 확인
cat ~/.m2/settings.xml | grep -A10 "<mirror>"
# 또는 프로젝트 pom.xml 저장소 확인
grep -A5 "<repository>" pom.xml
# 2. Nexus 서버 접근 가능 여부 확인
curl -v http://nexus.internal.company.com:8081/repository/maven-public/
# 또는
wget -q --spider http://nexus.internal.company.com:8081/
# 3. 폐쇄망 환경: ~/.m2/settings.xml 에 미러 등록
# <settings>
# <mirrors>
# <mirror>
# <id>internal-nexus</id>
# <mirrorOf>*</mirrorOf>
# <url>http://nexus.internal.company.com:8081/repository/maven-public/</url>
# </mirror>
# </mirrors>
# </settings>
# 4. 로컬 캐시 강제 갱신 (캐시 오염 의심 시)
mvn clean package -DskipTests -U
# -U : 최신 버전 강제 확인 (스냅샷 포함)
원인: 서로 다른 라이브러리에 동일한 클래스가 들어 있습니다. 의존성 버전 충돌로 같은 클래스가 클래스패스에 두 번 올라간 상태입니다.
# 1. 충돌 클래스가 어느 JAR에서 오는지 확인
mvn dependency:tree -Dverbose | grep "StringUtils"
# 2. 특정 라이브러리 의존성 경로 확인
mvn dependency:tree -Dincludes=com.example:legacy-util
# 3. pom.xml에서 exclusion으로 중복 제거
# <dependency>
# <groupId>com.example</groupId>
# <artifactId>framework-core</artifactId>
# <version>2.0.0</version>
# <exclusions>
# <exclusion>
# <groupId>com.example</groupId>
# <artifactId>legacy-util</artifactId>
# </exclusion>
# </exclusions>
# </dependency>
# 4. 제거 후 재빌드
mvn clean package -DskipTests
# Gradle에서 같은 문제
./gradlew dependencies | grep "StringUtils"
# build.gradle에서 강제 버전 지정
# configurations.all {
# resolutionStrategy.force 'com.example:util:2.0.0'
# }
심화 — 같은 커밋이 같은 산출물을 보장하지 않는다
심화: 빌드 캐시와 재현성 — 잠기지 않은 의존성은 떠내려간다
"빌드가 성공했다"와 "그 빌드를 언제 다시 돌려도 똑같이 나온다"는 다른 이야기입니다. 배포 사고의 상당수는 후자가 깨질 때 조용히 시작됩니다.
- 로컬 캐시가 속도를 주지만 진실을 가립니다: Maven은 받은 아티팩트를
~/.m2/repository에, Gradle은 build cache와 증분 상태를, npm은 lockfile과node_modules를 재사용해 빌드를 빠르게 합니다. 문제는 캐시가 이미 데워진 CI 에이전트에서만 성공하고, 캐시가 빈 새 에이전트에서는 실패하거나 다른 버전을 끌어올 수 있다는 점입니다. - 재현성을 깨는 세 가지: (1)
-SNAPSHOT의존성 — 같은 좌표인데 내용이 배포마다 바뀝니다. (2) 버전 범위 — Maven[1.0,2.0), Gradle의+나latest.release. (3) lockfile 부재 — transitive 의존성이 해석 시점마다 달라집니다. - lock의 역할과 도구별 차이:
package-lock.json·pnpm-lock.yaml은 transitive까지 정확한 버전과 체크섬을 박제하고,npm ci·--frozen-lockfile은 그 lock을 그대로 재현합니다. Gradle은 dependency locking을 명시적으로 켜야 하고(dependencyLocking), Maven은 표준 lock이 없어 Enforcer 플러그인의requireReleaseDeps나 고정 버전 관리로 대체합니다. - 한계 — lock도 못 막는 것: lockfile이 버전을 박제해도, 아티팩트 저장소에서 같은 좌표의 파일이 교체(재배포)되면 내용이 바뀔 수 있습니다. 이건 체크섬 검증으로만 막습니다. 그래서 릴리스 아티팩트는 immutable 저장소에 두고 체크섬을 기록합니다 — lock은 "무엇을 받을지"를 고정할 뿐, "저장소가 그걸 안 바꾼다"까지 보장하지는 않습니다.
상황: 핫픽스를 위해 릴리스 태그 v1.4.0을 다시 체크아웃해 빌드했습니다. 소스는 한 줄도 바뀌지 않았는데, 배포하자 QA 때는 없던 NoSuchMethodError가 특정 화면에서 납니다.
원인: 의존성 하나가 1.4.0-SNAPSHOT(또는 버전 범위·+)로 선언돼 있었습니다. 릴리스 태그의 소스는 고정이지만 그 의존성은 mutable이라, 지난달 이후 저장소에 올라간 새 스냅샷을 재빌드가 끌어와 transitive 트리가 바뀐 것입니다. lockfile이 없어 아무도 이 변화를 눈치채지 못했습니다.
진단: QA 당시 빌드 로그(또는 아티팩트)와 지금 빌드의 mvn dependency:tree(Gradle은 ./gradlew dependencies)를 diff합니다. 바뀐 좌표가 드러나면 grep -E 'SNAPSHOT|latest.release'와 버전 범위 표기를 pom/build 파일에서 찾아 '떠내려가는' 선언을 특정합니다. ~/.m2에서 해당 스냅샷의 타임스탬프를 보면 언제 갈렸는지도 확인됩니다.
해결: 릴리스 빌드에서는 SNAPSHOT·버전 범위 의존성을 금지합니다(Maven Enforcer requireReleaseDeps, Gradle dependency locking). 문제 의존성을 정확한 릴리스 버전으로 고정하고 lock을 커밋한 뒤, CI를 캐시가 비워진 새 에이전트(또는 임시 -Dmaven.repo.local)에서 돌려 캐시가 진짜 문제를 가리지 않는지 확인합니다. 재발 방지로 릴리스 아티팩트는 체크섬을 기록해 immutable 저장소에 보관합니다.
실제 업무에서 이 지식이 쓰이는 상황:
인프라 엔지니어가 빌드 도구를 직접 다루는 장면 세 가지입니다.
1. 배포 파이프라인 구성 시:
# Jenkins/GitLab CI에서 빌드 스테이지 예시
# Stage: Build
mvn clean package -DskipTests -Pprod -B
# -B : 배치 모드 (프롬프트 없음, CI 필수)
# 산출물 경로 확인 후 배포 단계로 전달
ARTIFACT=$(ls target/*.war | head -1)
echo "빌드 산출물: $ARTIFACT"
ls -lh "$ARTIFACT"
2. 빌드 실패 원인 분류 — 첫 30초 판단:
# Maven 빌드 실패 시 원인 찾기
mvn clean package -DskipTests 2>&1 | grep -E "^\\[ERROR\\]" | head -20
# 컴파일 오류
# [ERROR] .../UserService.java:[45,12] cannot find symbol
# → 소스 코드 문제, 개발팀 전달
# 의존성 오류
# [ERROR] Cannot access central (https://repo.maven.apache.org) ...
# → 네트워크/Nexus 문제, 인프라팀 처리
# 테스트 실패 (테스트 포함 빌드 시)
# [ERROR] Tests run: 10, Failures: 2, Errors: 0
# → 개발팀 전달 (인프라 문제 아님)
3. 빌드 산출물 버전 추적:
# WAR/JAR 내부에서 버전 확인
unzip -p target/myapp.war META-INF/MANIFEST.MF
# 또는
jar tf target/myapp.jar | grep -i "manifest\|pom.properties"
unzip -p target/myapp.jar META-INF/maven/com.example/myapp/pom.properties
빌드 도구를 이해하면 배포 파이프라인 구성과 빌드 실패 원인 분류를 개발팀 없이 처리할 수 있습니다.
명령어·단축키 빠른 참조
이 모듈에서 다룬 빌드·의존성 명령을 실전 옵션과 함께 모았습니다. "예" 열 조합은 배포 파이프라인에 그대로 넣어도 됩니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
mvn clean package | Maven 빌드(compile→test→package) | mvn -o clean package -DskipTests -Pprod (오프라인·테스트생략·prod 프로파일) |
mvn -B | CI 배치 모드(프롬프트 없음) | mvn clean package -DskipTests -B |
mvn -U | 원격 최신 의존성 강제 갱신 | 캐시 오염 의심 시 mvn clean package -U |
mvn dependency:tree | 의존성 트리·충돌 확인 | mvn dependency:tree | grep "omitted for conflict" |
mvn dependency:analyze | 미사용·누락 의존성 점검 | 런타임 ClassNotFound 추적 |
./gradlew clean build | Gradle Wrapper 빌드(버전 고정) | ./gradlew clean build -x test (테스트 제외) |
./gradlew dependencies | Gradle 의존성 트리 | ./gradlew bootJar / bootWar (산출물 생성) |
npm ci | lock 기준 재현 설치(CI) | node_modules 삭제 후 package-lock.json 그대로 설치 |
npm run build | 프론트엔드 운영 빌드 | 산출물 dist/·build/에 정적 파일 생성 |
pnpm install --frozen-lockfile | pnpm CI 설치(lock 고정) | 이어서 pnpm run build |
ls -lh target/ | 빌드 산출물 크기 확인 | ls -lh target/*.war target/*.jar |
unzip -p … MANIFEST.MF | WAR/JAR 내부 버전 추적 | unzip -p target/myapp.war META-INF/MANIFEST.MF |
관련 모듈로 더 깊이:
- Git/GitLab 브랜치 전략과 릴리즈 관리 — 빌드의 입력이 되는 소스 코드 형상관리와 브랜치 전략
- WAR/JAR/정적파일 배포와 배포 스크립트 작성 — 빌드 산출물(WAR/JAR)을 실제 서버에 배포하는 절차
- DDL 반영 절차와 Migration 안전 운영 — 산출물과 함께 반영해야 할 DB 스키마 변경 관리
다음 모듈에서는 이렇게 만든 산출물을 운영 DB에 반영하는 스키마 변경 절차를 다룹니다.