Java 및 Spring 기반 프로젝트를 개발할 때, 프로젝트의 빌드, 의존성 관리, 배포 등의 복잡한 과정을 자동화하는 빌드 도구는 선택이 아닌 필수입니다. 이 분야에서 가장 대표적인 두 거목, 바로 Maven과 Gradle인데요. 각각의 특징과 장단점, 그리고 어떤 상황에 어떤 도구를 선택하는 것이 좋을지 심층적으로 비교 분석해 보겠습니다.
src/main/java
, src/test/java
)와 빌드 라이프사이클을 따르면, 복잡한 설정 없이도 손쉽게 프로젝트를 빌드할 수 있도록 설계되었습니다.pom.xml
(Project Object Model)default
: 프로젝트 컴파일, 테스트, 패키징, 배포 등 핵심 빌드 작업을 수행합니다.clean
: 이전 빌드에서 생성된 산출물(예: target
디렉터리)을 삭제합니다.site
: 프로젝트 문서, 리포트 등을 생성하여 웹사이트 형태로 제공합니다.phase
로 구성됩니다. 특정 phase
를 실행하면, 해당 phase
이전까지의 모든 phase
가 순차적으로 실행됩니다.mvn package
명령 실행 시, default
라이프사이클의 validate
→ compile
→ test
→ package
phase가 순서대로 실행됩니다. (clean
이나 site
라이프사이클의 phase는 별도 호출 필요)default
라이프사이클 Phase 순서:validate
→ compile
→ test
→ package
→ verify
→ install
→ deploy
<dependencies>
태그 내에 필요한 라이브러리(artifact)와 버전을 명시합니다.<scope>
): 라이브러리가 사용되는 범위와 클래스패스 포함 시점을 제어합니다.compile
(기본값): 컴파일 시 필요, 런타임 시 클래스패스 및 최종 패키징에 포함.provided
: 컴파일 시 필요하나, 런타임에는 JDK나 서블릿 컨테이너(WAS) 등에 의해 제공되므로 최종 패키징에는 불필요. (예: javax.servlet-api
)runtime
: 컴파일 시에는 불필요하나, 런타임 시 필요. (예: JDBC 드라이버)test
: 테스트 코드 컴파일 및 실행 시에만 필요. 최종 패키징에는 미포함.system
: provided
와 유사하나, 로컬 시스템 경로에서 직접 JAR 파일을 지정. (사용 비권장)import
: 다른 POM 파일의 <dependencyManagement>
섹션을 가져올 때 사용 (주로 BOM - Bill of Materials 관리).<dependencyManagement>
: 여러 모듈에서 공통으로 사용되는 의존성의 버전을 한 곳에서 중앙 집중적으로 관리하여 버전 충돌을 방지하고 일관성을 유지합니다.pom.xml
파일은 프로젝트가 커질수록 장황해지고 가독성이 떨어질 수 있습니다.build.gradle
(Groovy DSL): 전통적으로 많이 사용되어 온 방식입니다.build.gradle.kts
(Kotlin DSL): 최근 강력하게 권장되는 방식으로, 타입 안정성, IDE의 우수한 자동완성 및 리팩토링 지원 등의 장점을 제공합니다.gradlew
):./gradlew build
와 같이 사용)dependencies
블록 내에 필요한 라이브러리를 선언합니다.implementation
: 해당 모듈 내부에서만 사용하는 의존성을 의미합니다. 이 의존성이 변경되어도 이 모듈을 사용하는 다른 모듈은 재컴파일할 필요가 없어 빌드 시간을 단축하는 데 기여합니다. (가장 일반적으로 권장)api
: 해당 모듈의 공개 API(public 메서드 시그니처 등)에 노출되는 의존성을 의미합니다. 이 의존성이 변경되면 이 모듈을 사용하는 다른 모듈도 재컴파일해야 합니다.compileOnly
: 컴파일 시에만 필요하고, 런타임에는 제공되거나 불필요한 의존성입니다 (Maven의 provided
와 유사).runtimeOnly
: 런타임 시에만 필요한 의존성입니다 (Maven의 runtime
과 유사).testImplementation
: 테스트 코드에서만 사용하는 의존성입니다 (Maven의 test
와 유사).구분 | Maven | Gradle |
---|---|---|
설정 파일 | pom.xml (XML) | build.gradle (Groovy DSL) / build.gradle.kts (Kotlin DSL) |
빌드 선언 방식 | 완전 선언적 (구문 단순, 가독성 우수) | 선언적 + 스크립트 혼합 (유연성, 표현력 우수) |
라이프사이클/Task | 3가지 공식 라이프사이클 (Phase 고정, 예측 가능) | Task 그래프 자동 생성, 커스텀 자유 (Task 단위 정의 및 재사용) |
의존성 관리 | <scope> (compile, provided, runtime, test 등) | configurations (implementation, api, compileOnly, runtimeOnly 등) |
성능 | 안정적이나 대규모 프로젝트에서 느릴 수 있음 | 증분 빌드, 빌드 캐시, 병렬 실행 등으로 매우 빠름 (대규모 프로젝트 시 2~10배 이상) |
학습 곡선 | 낮음 (XML 및 기본 개념 이해) | 상대적으로 높음 (DSL 및 Gradle 내부 동작/API 학습 필요) |
플러그인/생태계 | 방대한 Maven 플러그인 생태계, 오랜 기간 검증 | Maven/Ant 플러그인 호환 + 자체 플러그인 다수, 활발한 커뮤니티 |
멀티모듈 지원 | 표준 지원 (Parent POM을 통한 상속 및 집계) | 고성능 멀티모듈 지원, 설정 공유 및 분리 자유도 높음, 빌드 최적화 우수 |
주요 사용처 | 레거시/엔터프라이즈 Java, 전통적인 Spring 프로젝트 | Android Studio (공식 빌드 도구), 현대적인 Spring Boot, 대규모 모놀리포 |
Spring Initializr (https://start.spring.io
)에서는 프로젝트 생성 시 Maven과 Gradle 중 자유롭게 빌드 도구를 선택할 수 있습니다. 어떤 상황에 어떤 도구가 더 적합할까요?
상황 | 추천 도구 | 이유 |
---|---|---|
단순/소규모 서비스, 팀원들이 Maven에 익숙하고 빠른 온보딩이 중요할 때 | Maven | 구조와 라이프사이클이 고정되어 있어 팀원 적응이 쉽고, 예측 가능한 빌드 환경 제공. |
Android 앱 개발, Kotlin 기반 프로젝트 | Gradle (Kotlin DSL) | Google 및 JetBrains의 공식 지원, 풍부한 Android 관련 플러그인, Kotlin 언어와의 시너지 효과. |
대규모 모놀리식 프로젝트, 다수의 모듈 분리 및 빌드 시간 단축이 매우 중요할 때 | Gradle | 강력한 멀티모듈 지원, 병렬 빌드 및 빌드 캐시를 통한 빌드 시간 절감 효과 극대화. |
기존 Maven 기반 레거시 시스템 유지보수 또는 확장 | Maven | 기존 pom.xml 생태계와의 호환성 및 축적된 플러그인 자산 활용 용이. |
CI/CD 파이프라인에서 빌드 병목 현상이 심각하여 최적화가 필요할 때 | Gradle | 빌드 캐시(로컬/원격) 및 병렬 실행을 통해 빌드 파이프라인의 전체 실행 시간 단축 가능. |
최신 기술 트렌드 및 빌드 성능을 중요시하는 신규 프로젝트 | Gradle (Kotlin DSL) | 타입 안정성, 우수한 IDE 지원, 뛰어난 빌드 성능 향상 등의 이점으로 빠르게 표준으로 자리 잡는 추세. |
궁극적으로는 프로젝트의 특성(규모, 복잡성), 팀의 기술적 숙련도, 빌드 성능 요구사항, CI/CD 환경 등을 종합적으로 고려하여 가장 적합한 도구를 선택하는 것이 중요합니다.
라이브러리가 프로젝트 내에서 언제, 어떤 범위로 필요한지를 정의하는 것은 의존성 관리의 핵심입니다.
역할 (라이브러리가 필요한 시점/범위) | Maven <scope> | Gradle Configuration | 최종 패키징(JAR/WAR)에 포함 여부 |
---|---|---|---|
컴파일 시 & 런타임 시 모두 필요 | compile (기본) | implementation / api | O |
컴파일 시에만 필요 (런타임에는 외부 제공) | provided | compileOnly | X |
런타임 시에만 필요 (컴파일 시에는 불필요) | runtime | runtimeOnly | O |
테스트 코드 컴파일 및 실행 시에만 필요 | test | testImplementation | X |
💡 Gradle의 api
vs implementation
이해하기:
implementation
: 의존성을 해당 모듈 내부로 숨겨 캡슐화를 강화합니다. 이 의존성의 변경이 이 모듈을 사용하는 다른 모듈에게 전파되지 않아 불필요한 재컴파일을 줄여 빌드 속도를 높입니다. 대부분의 의존성은 implementation
으로 선언하는 것이 권장됩니다.api
: 의존성이 해당 모듈의 공개 API(예: public 메서드의 파라미터 타입, 리턴 타입)를 통해 외부로 노출될 때 사용합니다. 이 의존성이 변경되면, 이 모듈을 사용하는 다른 모듈도 영향을 받아 재컴파일해야 합니다. 주로 라이브러리 프로젝트에서 다른 모듈에게 특정 타입을 전달해야 할 때 사용됩니다.간단한 Spring Boot 웹 프로젝트의 의존성 설정을 통해 두 도구의 차이를 살펴보겠습니다.
Maven 예시 (pom.xml
)
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-maven-app</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<spring-boot.version>3.2.5</spring-boot.version>
<junit.version>5.10.2</junit.version>
<mysql.connector.version>8.4.0</mysql.connector.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter (compile scope by default) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- MySQL Connector (runtime scope) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.connector.version}</version>
<scope>runtime</scope>
</dependency>
<!-- JUnit Jupiter (test scope) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Lombok (provided scope, compile time only) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
Gradle (Kotlin DSL) 예시 (build.gradle.kts
)
plugins {
id("org.springframework.boot") version "3.2.5"
id("io.spring.dependency-management") version "1.1.4"
kotlin("jvm") version "1.9.24"
kotlin("plugin.spring") version "1.9.24" // For Spring specific Kotlin features
}
group = "com.example"
version = "1.0.0"
java {
sourceCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
val junitVersion = "5.10.2"
val mysqlConnectorVersion = "8.4.0"
val lombokVersion = "1.18.30"
dependencies {
// Spring Boot Web Starter (implementation configuration)
implementation("org.springframework.boot:spring-boot-starter-web")
// For Kotlin with Jackson
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
// MySQL Connector (runtimeOnly configuration)
runtimeOnly("mysql:mysql-connector-j:$mysqlConnectorVersion")
// Lombok (compileOnly for compilation, annotationProcessor for processing)
compileOnly("org.projectlombok:lombok:$lombokVersion")
annotationProcessor("org.projectlombok:lombok:$lombokVersion")
// JUnit Jupiter (testImplementation configuration)
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion") // or testImplementation
}
tasks.withType<Test> {
useJUnitPlatform()
}
// Optional: For Kotlin Spring plugin specific configurations
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
<scope>
와 Gradle의 configurations
는 라이브러리 포함 범위와 시점을 제어하는 중요한 개념입니다. 특히 Gradle의 implementation
구성은 의존성 캡슐화를 통해 빌드 최적화에 크게 기여합니다.