빌드 도구 — Maven과 Gradle 이해하기
build.gradle 파일은 매일 보는데, 이게 정확히 뭘 하는 건지 설명할 수 있는가?
dependencies에 뭔가를 추가하면 라이브러리가 알아서 들어오고,./gradlew build를 치면 jar 파일이 뚝 떨어진다. 편리하지만, 그 안에서 무슨 일이 일어나는지 모른 채 쓰고 있었다면 이 글이 도움이 될 것이다.
▸ TIP 이 글의 코드 예제를 직접 실행해보고 싶다면 Java 기본기 핸드북을 확인해보세요.
빌드 도구가 왜 필요한가
코드를 작성하고 실행하기까지는 생각보다 많은 단계가 있다.
[소스 코드] → 컴파일 → 테스트 → 패키징 → 배포
↑
외부 라이브러리 다운로드 & 클래스패스 설정
작은 프로젝트라면 javac로 컴파일하고 java로 실행하면 된다. 하지만 프로젝트가 커지면 문제가 생긴다.
- 의존성 관리: Spring Boot를 쓰려면 수십 개의 jar가 필요하다. 하나하나 다운로드할 것인가?
- 테스트 자동화: 코드를 바꿀 때마다 테스트를 수동으로 돌릴 것인가?
- 패키징: jar, war 같은 배포 파일을 매번 수작업으로 만들 것인가?
- 환경 일관성: 내 PC에서는 되는데 팀원 PC에서는 안 된다면?
빌드 도구는 이 모든 과정을 자동화한다. Java 생태계에서 주로 쓰이는 빌드 도구는 두 가지다.
| 도구 | 등장 시기 | 설정 파일 | 설정 방식 |
|---|---|---|---|
| Maven | 2004년 | pom.xml | XML 선언형 |
| Gradle | 2012년 | build.gradle / build.gradle.kts | Groovy/Kotlin DSL |
역사적으로 Ant → Maven → Gradle 순서로 발전해왔다. Ant는 자유도가 높지만 표준이 없었고, Maven이 "컨벤션 오버 컨피규레이션"을 도입해 표준을 만들었다. Gradle은 Maven의 장점을 취하면서 더 유연하고 빠른 빌드를 제공한다.
Maven 기본 — pom.xml 구조
Maven 프로젝트의 핵심은 pom.xml(Project Object Model) 파일이다.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<!-- GAV 좌표: 이 프로젝트의 고유 식별자 -->
<groupId>com.example</groupId> <!-- 조직/그룹 ID -->
<artifactId>my-app</artifactId> <!-- 프로젝트 이름 -->
<version>1.0.0</version> <!-- 버전 -->
<packaging>jar</packaging> <!-- 패키징 방식 -->
<!-- Java 버전 설정 -->
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<!-- 의존성 목록 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>
<!-- 테스트에서만 사용 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
GAV 좌표
Maven 생태계에서 모든 라이브러리는 GAV 좌표로 식별된다.
- GroupId: 조직이나 프로젝트 그룹 (예:
org.springframework) - ArtifactId: 프로젝트 이름 (예:
spring-core) - Version: 버전 (예:
6.1.2)
이 세 값으로 Maven Central에서 jar를 다운로드한다. 로컬에는 ~/.m2/repository/ 아래에 저장된다.
Maven 디렉토리 구조
별도 설정 없이 다음 구조를 따르면 알아서 빌드된다. 이 구조는 Gradle에서도 동일하다.
my-app/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/ ← 소스 코드
│ │ └── resources/ ← 설정 파일
│ └── test/
│ ├── java/ ← 테스트 코드
│ └── resources/ ← 테스트용 설정 파일
└── target/ ← 빌드 결과물 (자동 생성)
Maven 라이프사이클
Maven의 가장 큰 특징은 표준 빌드 라이프사이클이다. 빌드 과정을 단계(phase)로 나누고 순서대로 실행한다.
validate → compile → test → package → verify → install → deploy
| 단계 | 하는 일 |
|---|---|
validate | 프로젝트 설정이 올바른지 검증 |
compile | src/main/java 소스를 컴파일 |
test | 단위 테스트 실행 |
package | 컴파일된 코드를 jar/war로 패키징 |
install | 패키지를 로컬 저장소(~/.m2)에 설치 |
deploy | 패키지를 원격 저장소에 배포 |
핵심은 누적 실행이다. mvn package를 실행하면 validate → compile → test → package가 모두 실행된다.
# 실무에서 자주 쓰는 명령어
mvn compile # 컴파일만
mvn test # 테스트까지
mvn clean package # 깨끗하게 빌드 후 패키징 (가장 많이 씀)
mvn clean package -DskipTests # 테스트 건너뛰기 (급할 때만)
Maven 의존성 관리
scope — 의존성의 사용 범위
| scope | 컴파일 | 테스트 | 런타임 | 패키징 | 대표 예시 |
|---|---|---|---|---|---|
compile (기본) | O | O | O | O | spring-core |
test | X | O | X | X | JUnit, Mockito |
provided | O | O | X | X | Servlet API |
runtime | X | O | O | O | JDBC 드라이버 |
공부하다 보니 provided가 좀 헷갈렸다. 컴파일할 때는 필요하지만, 실행 환경(톰캣 등)이 이미 갖고 있으니 jar에 안 넣는다는 뜻이다. war 배포할 때 Servlet API가 이중으로 들어가면 충돌이 나기 때문에 이 구분이 중요하다.
전이 의존성 (Transitive Dependency)
A가 B를 의존하고, B가 C를 의존하면 — A는 자동으로 C도 가져온다.
my-app → spring-boot-starter-web → spring-webmvc → spring-core
편리하지만, 내가 추가하지 않은 라이브러리가 들어오면서 버전 충돌이 발생할 수 있다. mvn dependency:tree로 의존성 트리를 확인하는 습관이 중요하다.
Gradle 기본 — build.gradle 구조
Gradle은 Groovy DSL 또는 Kotlin DSL로 빌드 스크립트를 작성한다. XML보다 간결하고, 조건문이나 반복문도 쓸 수 있다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '1.0.0'
// 자바 21 사용
java {
sourceCompatibility = JavaVersion.VERSION_21
}
repositories {
mavenCentral() // Maven Central에서 의존성 다운로드
}
dependencies {
// 컴파일 + 런타임에 사용
implementation 'org.springframework.boot:spring-boot-starter-web'
// 컴파일 시에만 (Lombok은 런타임에 불필요)
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// 런타임에만 필요
runtimeOnly 'com.mysql:mysql-connector-j'
// 테스트에서만 사용
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
Kotlin DSL(build.gradle.kts)도 있다. 문법만 다르고 기능은 동일하다. 타입 안전성과 IDE 자동완성이 더 좋아서 최근에는 .kts를 선호하는 추세다.
같은 의존성을 두 도구로 비교하면 간결함의 차이가 확연하다.
<!-- Maven: 5줄 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>
// Gradle: 1줄
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.0'
Gradle 태스크와 의존성
의존성 설정(Configuration)
Gradle에서는 Maven의 <scope>에 해당하는 개념을 configuration이라고 부른다.
| Gradle 설정 | Maven scope 대응 | 설명 |
|---|---|---|
implementation | compile | 컴파일 + 런타임, 소비자에게 노출 안 됨 |
api | compile | 컴파일 + 런타임, 소비자에게 노출됨 |
compileOnly | provided | 컴파일에만 사용 |
runtimeOnly | runtime | 런타임에만 사용 |
testImplementation | test | 테스트 컴파일 + 런타임에 사용 |
implementation vs api는 면접에서도 종종 나오는 포인트다.
모듈 A (library)
├── api: commons-lang3 → 모듈 B에서도 사용 가능
└── implementation: guava → 모듈 B에서 사용 불가
모듈 B (application)
└── implementation: 모듈 A
implementation으로 선언하면 의존성이 모듈 내부에 캡슐화된다. 컴파일 속도도 빨라지고, 내부 구현을 바꿔도 소비자 모듈에 영향이 없다. 특별한 이유가 없다면 implementation을 기본으로 쓰자.
Gradle 태스크
# 자주 쓰는 명령어
./gradlew build # 컴파일 + 테스트 + jar 생성
./gradlew clean build # 깨끗하게 다시 빌드
./gradlew bootRun # Spring Boot 실행
./gradlew dependencies # 의존성 트리 확인
커스텀 태스크도 만들 수 있다. Maven에서는 플러그인을 만들어야 하는 일이 Gradle에서는 몇 줄이면 된다.
// 커스텀 태스크 정의
tasks.register('hello') {
doLast {
println '안녕하세요, Gradle!'
}
}
의존성 충돌 해결
라이브러리 A가 guava 31.0을 요구하고, B가 guava 30.0을 요구한다면? Maven과 Gradle은 서로 다른 전략으로 해결한다.
Maven: Nearest-First
의존성 트리에서 루트에 가장 가까운 버전을 선택한다. 같은 깊이면 먼저 선언된 쪽이 이긴다.
my-app
├── A (guava 31.0) ← 깊이 1: 선택됨
└── B → C (guava 30.0) ← 깊이 2: 무시됨
Gradle: Newest-First
가장 높은 버전을 선택한다. 대부분 상위 버전이 하위 호환되므로 더 안전한 편이다.
충돌을 직접 해결하는 방법
1. exclude — 특정 전이 의존성 제거
// Gradle
implementation('com.example:library-a:1.0.0') {
exclude group: 'com.google.guava', module: 'guava'
}
<!-- Maven -->
<dependency>
<groupId>com.example</groupId>
<artifactId>library-a</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
2. BOM(Bill of Materials) — 버전 통합 관리
BOM은 여러 라이브러리의 버전을 한 곳에서 관리한다. Spring Boot에서 starter 의존성에 버전을 안 써도 되는 이유가 BOM 덕분이다.
// Gradle: BOM 사용
dependencies {
implementation platform('org.springframework.boot:spring-boot-dependencies:3.2.0')
implementation 'org.springframework.boot:spring-boot-starter-web' // 버전 생략 가능
}
3. 버전 강제 지정
// Gradle: 특정 버전을 강제
configurations.all {
resolutionStrategy {
force 'com.google.guava:guava:31.1-jre'
}
}
Maven vs Gradle 비교
| 비교 항목 | Maven | Gradle |
|---|---|---|
| 설정 파일 | pom.xml (XML) | build.gradle (Groovy/Kotlin DSL) |
| 빌드 속도 | 느림 (캐시 없음) | 빠름 (증분 빌드, 빌드 캐시) |
| 의존성 충돌 | nearest-first | newest-first |
| 커스터마이징 | 플러그인에 의존 | 태스크로 자유롭게 작성 |
| 러닝 커브 | 낮음 (표준이 명확) | 중간 (DSL 학습 필요) |
| 빌드 캐시 | 없음 | 로컬 + 원격 캐시 지원 |
| 데몬 프로세스 | 없음 | Gradle Daemon (JVM 재사용) |
Gradle이 빠른 이유는 세 가지다.
- 증분 빌드: 변경된 파일만 다시 컴파일한다
- 빌드 캐시: 이전 빌드 결과를 캐시해두고, 입력이 같으면 재사용한다
- Gradle Daemon: JVM을 백그라운드에 띄워두고 재사용한다
대규모 프로젝트에서 Gradle이 Maven보다 2~10배 빠른 것으로 알려져 있다. Spring Framework 자체도 Maven에서 Gradle로 전환했다.
어떤 걸 선택해야 하는가? 새 프로젝트라면 Gradle을 추천한다. 기존 Maven 프로젝트를 굳이 전환할 필요는 없다. 중요한 것은 도구가 아니라 의존성 관리와 빌드 프로세스를 이해하는 것이다.
Multi-module 프로젝트
프로젝트가 커지면 모듈을 분리해야 한다.
my-project/
├── settings.gradle ← 모듈 목록 정의
├── build.gradle ← 루트 (공통 설정)
├── core/ ← 공통 모듈 (엔티티, 유틸)
│ ├── build.gradle
│ └── src/
├── api/ ← API 서버 모듈
│ ├── build.gradle
│ └── src/
└── batch/ ← 배치 모듈
├── build.gradle
└── src/
// settings.gradle — 모듈 등록
rootProject.name = 'my-project'
include 'core', 'api', 'batch'
// 루트 build.gradle — 공통 설정
subprojects {
apply plugin: 'java'
group = 'com.example'
version = '1.0.0'
java { sourceCompatibility = JavaVersion.VERSION_21 }
repositories { mavenCentral() }
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
}
// api/build.gradle — 하위 모듈
plugins { id 'org.springframework.boot' }
dependencies {
// 같은 프로젝트의 core 모듈 의존
implementation project(':core')
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.mysql:mysql-connector-j'
}
project(':core')로 같은 프로젝트 내 모듈을 참조한다. core에 엔티티와 공통 유틸을 두고, api와 batch에서 가져다 쓰는 패턴이 실무에서 흔하다. Maven에서도 부모 POM + <modules>로 동일한 구조를 만들 수 있다.
Gradle Wrapper — gradlew의 역할
프로젝트에 Gradle 버전 정보와 실행 스크립트를 포함시켜서, Gradle을 설치하지 않아도 빌드할 수 있게 하는 도구다.
my-project/
├── gradlew ← Linux/Mac용 실행 스크립트
├── gradlew.bat ← Windows용 실행 스크립트
└── gradle/wrapper/
├── gradle-wrapper.jar
└── gradle-wrapper.properties ← Gradle 버전 정보
# gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
왜 중요한가:
- 설치 불필요:
./gradlew만 있으면 지정된 Gradle을 자동 다운로드한다 - 버전 일관성: 모든 개발자와 CI 서버에서 동일한 버전으로 빌드한다
- 재현 가능한 빌드: 6개월 후에 클론받아도 같은 환경에서 빌드된다
실무에서는 gradle 명령어 대신 항상 ./gradlew를 사용하는 것이 원칙이다.
# 버전 업그레이드도 간단하다
./gradlew wrapper --gradle-version 8.6
Maven에도 Maven Wrapper(
mvnw)가 있다. 같은 원리로 동작한다.
정리 테이블
| 개념 | Maven | Gradle |
|---|---|---|
| 설정 파일 | pom.xml | build.gradle / .kts |
| 의존성 식별 | GAV 좌표 | 동일 (group:name:version) |
| 의존성 저장소 | ~/.m2/repository | ~/.gradle/caches |
| 빌드 단위 | Phase (라이프사이클) | Task |
| 의존성 범위 | scope | configuration |
| 의존성 충돌 | nearest-first | newest-first |
| 버전 통합 관리 | <dependencyManagement> | platform() |
| 멀티모듈 | 부모 POM + <modules> | settings.gradle + subprojects |
| Wrapper | mvnw | gradlew |
| 증분 빌드 / 캐시 | 미지원 | 지원 |
면접 포인트 요약
- 빌드 도구의 역할: 컴파일, 의존성 관리, 테스트, 패키징을 하나의 명령으로 처리
- Maven 핵심: GAV 좌표, 표준 라이프사이클, XML 선언형 설정
- Gradle 핵심: DSL 기반 유연한 설정, 증분 빌드와 캐시로 빠른 빌드
- implementation vs api: 의존성 캡슐화 여부. 기본은 implementation
- 의존성 충돌: Maven은 가까운 것 우선, Gradle은 최신 것 우선
- Wrapper: 빌드 환경의 일관성과 재현성을 보장
마무리
빌드 도구는 매일 쓰지만 깊이 파고들지 않기 쉬운 영역이다. ./gradlew build가 내부에서 뭘 하는지, 의존성은 어떻게 해석되는지 이해하고 나면 빌드 에러가 났을 때 당황하지 않고 원인을 찾을 수 있다.
정리하면:
- 빌드 도구는 컴파일 → 테스트 → 패키징 → 배포를 자동화한다
- Maven은 XML 기반, 표준 라이프사이클, nearest-first 의존성 해결
- Gradle은 DSL 기반, 태스크 단위 빌드, newest-first + 증분 빌드 + 캐시
- 멀티모듈로 공통 코드를 분리하면 대규모 프로젝트를 체계적으로 관리할 수 있다
- Wrapper(
gradlew,mvnw)로 빌드 환경의 일관성을 보장하자
다음 글에서는 자바 개발자 로드맵을 다룬다. 34편 시리즈의 마무리로, 지금까지 다룬 내용을 어떤 순서로 학습하면 좋을지 정리해볼 예정이다.