infra
Platform

모듈 맵

[Infra Ops] Maven/Gradle/npm 빌드와 산출물 관리

0 / 52 완료

펼치기
0 / 52 완료0%

인프라 운영 & SRE · 35 / 52

[Infra Ops] Maven/Gradle/npm 빌드와 산출물 관리

Maven pom.xml, Gradle build.gradle, npm/pnpm 빌드, 의존성 충돌 해결, 빌드 산출물 관리까지 — 인프라 엔지니어가 배포를 위해 알아야 할 빌드 실무

🚨INCIDENT ALERT
HIGH

배포 요청이 왔습니다. 개발팀이 "배포 해주세요"라며 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를 만듭니다.

Maven 라이프사이클과 pom.xml — Maven은 고정된 빌드 단계(validate→compile→test→package→verify→install→deploy)를 거쳐, mvn package는 항상 target/에 WAR/JAR를 생성. pom.xml에 의존성·플러그인·빌드 설정을 선언해 처음 보는 프로젝트도 동일 패턴으로 빌드확대

긴급 배포 요청이 왔습니다. 저장소를 클론했는데 mvn package를 실행하면 테스트가 30분씩 걸리고, 운영 프로파일로 빌드했는지 확인할 방법도 모릅니다. 빌드 로그 어딘가에 에러가 있는데 BUILD FAILURE 한 줄만 보입니다. 빌드 도구를 모르면 처음 보는 프로젝트를 배포할 때 판단 근거가 없습니다. Maven 라이프사이클을 이해하면 어느 단계에서 실패했는지, 테스트를 건너뛰려면 어떤 옵션을 쓰는지, 산출물이 어디 생기는지를 즉시 파악할 수 있습니다.

Maven 라이프사이클 (순서대로 실행):

clean → validate → compile → test → package → install → deploy
단계설명
cleantarget/ 디렉터리 삭제 (이전 빌드 산출물 제거)
compilesrc/main/java 소스 컴파일
testsrc/test/java 테스트 실행
packageWAR 또는 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 핵심 구조:

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/
GROOVY
// 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"'
JSON
{
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "build:prod": "REACT_APP_ENV=production react-scripts build",
    "test": "react-scripts test"
  }
}

CI 빌드 파이프라인 — Maven/Gradle 비교확대

실습

1Maven 운영 빌드 실행

실제 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
OUTPUT
[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가 개발 설정으로 패키징된 것. 파일명에 버전 번호가 있어야 배포 추적 가능
2의존성 충돌 확인

의존성 충돌은 빌드가 성공해도 런타임에 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
OUTPUT
[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 packageMaven 빌드(compile→test→package)mvn -o clean package -DskipTests -Pprod (오프라인·테스트생략·prod 프로파일)
mvn -BCI 배치 모드(프롬프트 없음)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 buildGradle Wrapper 빌드(버전 고정)./gradlew clean build -x test (테스트 제외)
./gradlew dependenciesGradle 의존성 트리./gradlew bootJar / bootWar (산출물 생성)
npm cilock 기준 재현 설치(CI)node_modules 삭제 후 package-lock.json 그대로 설치
npm run build프론트엔드 운영 빌드산출물 dist/·build/에 정적 파일 생성
pnpm install --frozen-lockfilepnpm CI 설치(lock 고정)이어서 pnpm run build
ls -lh target/빌드 산출물 크기 확인ls -lh target/*.war target/*.jar
unzip -p … MANIFEST.MFWAR/JAR 내부 버전 추적unzip -p target/myapp.war META-INF/MANIFEST.MF

관련 모듈로 더 깊이:

다음 모듈에서는 이렇게 만든 산출물을 운영 DB에 반영하는 스키마 변경 절차를 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

처음 보는 Java 프로젝트를 빌드해야 합니다. 저장소 루트에 pom.xml 파일이 있습니다. 이 프로젝트를 빌드하는 올바른 명령어는?

Q2

mvn clean package -DskipTests 에서 -DskipTests를 붙이는 실무적 이유는?

Q3

Jenkins CI 파이프라인에서 React 프론트엔드를 빌드합니다. 지난 주 로컬에서는 잘 됐는데 CI 서버에서는 라이브러리 버전이 달라 빌드가 실패합니다. 이를 방지하기 위한 올바른 npm 명령어는?

Q4

빌드 프로파일(build profile)을 사용하는 이유는?

Q5

Maven 빌드 라이프사이클에서 mvn package를 실행하면 그 앞 단계들은 어떻게 되나?

Q6

팀원마다 로컬에 설치된 Gradle 버전이 달라 빌드 결과가 갈린다. 프로젝트에 포함된 ./gradlew(Gradle Wrapper)를 쓰면?

Q7

[심화] 코드를 한 줄도 바꾸지 않고 같은 릴리스 태그를 다시 빌드했는데, 산출물에 포함된 의존성 버전이 지난번과 달라졌습니다. 재현성을 깨는 가장 직접적인 원인은?

Q8

[심화] 같은 태그를 재빌드했더니 라이브러리 버전이 달라져 운영에서 처음 보는 오류가 났습니다. 원인을 가장 빠르게 확정하는 진단은?

0 / 8 답변

🧪 실습으로 확인하기

Nginx 설치 및 기동

초급

Linux 서버에 Nginx를 설치하고 systemd 서비스로 등록하여 80포트에서 응답하는 상태까지 만든다.

40📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요