배포 요청이 왔습니다. 개발팀이 "배포 해주세요"라며 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 clean package 마지막 줄이 BUILD SUCCESS 인가 (BUILD FAILURE 면 로그 스크롤 업해서 ERROR 줄 찾기)
- ls -lh target/*.war 산출물 크기가 비정상적으로 작지 않은가 (1KB 이하면 빌드 문제)
- -Pprod 옵션이 있을 경우 빌드 로그에 'The following profiles are active: prod' 가 나타나는가
- 빌드 산출물 파일명에 버전 번호가 포함돼 있는가 (예: myapp-1.2.0.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 등)라면 pom.xml exclusion 적용 검토
- ./gradlew dependencies 출력에 FAILED 항목이 없는가
트러블슈팅
원인: 회사 내부 라이브러리를 사내 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'
# }
실제 업무에서 이 지식이 쓰이는 상황:
인프라 엔지니어가 빌드 도구를 직접 다루는 장면 세 가지입니다.
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
빌드 도구를 이해하면 배포 파이프라인 구성과 빌드 실패 원인 분류를 개발팀 없이 처리할 수 있습니다. 다음 모듈에서는 이렇게 만든 산출물을 운영 DB에 반영하는 스키마 변경 절차를 다룹니다.