본문 바로가기
Backend

Maven vs Gradle vs Ant : 빌드 방식의 변화

by okbear3 2025. 12. 27.

Java 개발을 시작하면 가장 먼저 마주하는 것이 바로 빌드 도구입니다.

pom.xml, build.gradle, build.xml… 각 파일들은 대체 무엇이고, 왜이렇게 많은 빌드 도구들이 존재할까요?

 

Spring 프로젝트 빌드에 사용되는 다양한 도구들의 차이점과 빌드 방식을 알아보려고합니다.


우선 빌드 도구인 Ant, Mave, Gradle을 도입하게 된 배경과, 목적을 한번 정리해봤습니다.

Java 빌드 도구의 역사와 배경

Ant (2000년 출시)

  • 배경 : Unix의 Make를 Java 세계로 가져오려는 시도, Java의 “Write Once, Run Anywhere”의 철학에 맞춰 플랫폼 독립적 빌드 필요
  • 특징
    • 절차적(Imperative) 방식의 XML 기반 빌드 스크립트
    • 의존성 관리 기능이 없음

Maven (2004년 출시)

  • 배경 : Ant의 복잡성과 반복성 문제 해결 (빌드 스크립트 중복, 수동적인 의존성 관리, 프로젝트 구조가 제각각)
  • 특징
    • XML 기반이지만 선언적(Declarative) 방식
    • 표준 디렉터리 구조 강제
    • 중앙저장소를 통한 의존성 관리
    • 라이프사이클 개념 도입

Gradle (2012년 출시)

  • 배경 : Maven의 XML 지옥 문제, 빌드 스크립트의 유연성과 빌드 속도 개선 필요
  • 특징
    • Groovy / Kotlin DSL (프로그래밍 가능)
    • Maven의 장점 + Ant의 유연성
    • 증분 빌드, 빌드 캐시로 성능 향상

Ant : 절차적 접근 (Imperative)

Ant 는 절차적(Imperative) 방식입니다. build.xml 빌드의 모든 단계를 명시적으로 정의할 수 있습니다.

<!-- build.xml -->
<project name="MyApp" default="build">

    <!-- 1단계: 디렉토리 생성 -->
    <target name="init">
        <mkdir dir="build/classes"/>
        <mkdir dir="dist"/>
    </target>

    <!-- 2단계: 컴파일 -->
    <target name="compile" depends="init">
        <javac srcdir="src" destdir="build/classes">
            <classpath>
                <fileset dir="lib">
                    <include name="**/*.jar"/>
                </fileset>
            </classpath>
        </javac>
    </target>

    <!-- 3단계: JAR 생성 -->
    <target name="jar" depends="compile">
        <jar destfile="dist/myapp.jar" basedir="build/classes">
            <manifest>
                <attribute name="Main-Class" value="com.example.Main"/>
            </manifest>
        </jar>
    </target>

    <!-- 4단계: 전체 빌드 -->
    <target name="build" depends="jar"/>

</project>

 

Ant의 문제점

  1. 수동으로 의존성 관리 : 개발자가 필요 라이브러리를 수동 다운로드하여, 전이 라이브러리 및 충돌 해결이 필요함
  2. 반복적인 빌드 스크립트 : 프로젝트마다 비슷한 빌드 스크립트를 반복되고, 대규모 프로젝트에서 스크립트가 구천줄로 늘어났습니다.
  3. 프로젝트 구조가 제각각 : 각 빌드 과정 (컴파일 → 테스트 → 빌드 등)을 target과 task로 유연하게 작성할 수 있다는 장점이 있지만, 프로젝트 구조의 표준이 없어, 각 프로젝트의 빌드 결과를 확인하기 위해서는 build.xml 스크립트의 분석이 필요합니다.

Q. target / task란?


Maven : 선언적 접근 (Declarative)

Ant의 문제를 해결하기 위해 Maven이 등장했습니다.

Maven은 선언적(Declarative) 방식으로, 무엇을 원하는지만 선언하면 빌드 과정은 Maven이 처리하게 됩니다.

<!-- pom.xml -->
<project>
    <modelVersion>4.0.0</modelVersion>

    <!-- "이 프로젝트가 무엇인지" 선언 -->
    <groupId>com.example</groupId>
    <artifactId>myapp</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <!-- "무엇이 필요한지" 선언 -->
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.3.30</version>
        </dependency>
    </dependencies>

    <!-- Maven이 자동으로 처리:
         - src/main/java 컴파일
         - src/test/java 테스트
         - target/ 에 결과물 생성
         - 의존성 다운로드 및 관리
    -->
</project>

프로젝트 및 라이브러리 등을 pom.xml 파일에 정의하면,

Maven이 의존성 다운로드 → 컴파일 → 테스트 → 빌드 과정을 자동으로 처리해줍니다.

 

Maven의 장점

1. 중앙화된 자동 의존성 관리

pom.xml 파일에서 필요한 라이브러리를 선언하게 되면, 자동으로 관련된 의존성을 다운로드하고 관리합니다.

의존성을 다운로드하는 레지스트리는 기본적으로 Maven Central Repository를 참조하지만, 이 경로는 Nexus와 같은 사설 레지스트리로 변경할 수 있습니다.

 <!-- Spring Boot Starter Web 하나만 선언 -->
 <dependencies>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
         <version>3.2.0</version>
     </dependency>
 </dependencies>

 // 이 한 줄이 자동으로 가져오는 것:
 spring-boot-starter-web
 ├── spring-web
 ├── spring-webmvc
 ├── tomcat-embed-core
 ├── jackson-databind
 │   ├── jackson-core
 │   └── jackson-annotations
 └── ... (총 30개 이상의 의존성)

이렇게 다운로드한 라이브러리는 로컬에 캐싱되어, 다른 프로젝트에서도 재사용이 가능합니다.

 

2. 표준화된 디렉터리 구조

# Maven 프로젝트 구조 (모든 Maven 프로젝트가 동일)
my-maven-project/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/          ← Java 소스 코드
│   │   └── resources/     ← 설정 파일, 리소스
│   └── test/
│       ├── java/          ← 테스트 코드
│       └── resources/     ← 테스트 리소스
└── target/                ← 빌드 결과물

표준화된 프로젝트 구조로 IDE에서 자동으로 구조 인식이 가능하고, 팀원 간 협업이 간편해집니다.

 

3. 빌드 라이프 사이클

mvn clean package 와 같이 명령어를 입력하는 경우 아래와 같은 빌드 절차가 실행됩니다.

  • clean ← target/ 디렉토리 삭제
  • validate ← 프로젝트 검증
  • compile ← src/main/java 컴파일
  • test ← src/test/java 테스트 실행
  • package ← JAR/WAR 파일 생성

Maven은 기존 Ant에 비해 자동화된 의존성 관리와 표준화된 프로젝트 빌드 방식으로, 개발자들의 프로젝트 관리 부담을 줄여주었습니다.

그러나 Maven의 경우 너무 프로젝트의 구조가 명확하기 때문에, 표준 프로젝트의 구조를 벗어나기 어렵고, 복잡한 설정의 경우 XML의 지옥에서 벗어나기 어려운 단점이 있습니다.

 

이를 개선하기 위한 빌드 도구로 Gradle이 등장하게 됩니다.


Gradle : 프로그래밍 가능한 선언적 접근

Gradle은 Maven과 Ant의 장점을 합친 빌드 방식입니다.

Groovy / Kotlin DSL을 이용하여 빌드 스크립트 자체를 프로그래밍 언어로 만들었습니다.

// build.gradle
plugins {
    id 'java'
    id 'application'
}

// "무엇인지" 선언
group = 'com.example'
version = '1.0.0'

// "무엇이 필요한지" 선언
dependencies {
    implementation 'org.springframework:spring-core:5.3.30'

    // 조건부 의존성 (Groovy 코드!)
    if (project.hasProperty('enableKafka')) {
        implementation 'org.springframework.kafka:spring-kafka'
    }
}

// 프로그래밍 가능! (Maven에서 불가능)
tasks.register('customTask') {
    doLast {
        println "현재 환경: ${project.findProperty('env') ?: 'dev'}"

        // 동적으로 파일 처리
        fileTree('src/main/resources').each { file ->
            if (file.name.endsWith('.properties')) {
                println "속성 파일 발견: ${file.name}"
            }
        }
    }
}

// 기존 태스크 커스터마이징
tasks.named('jar') {
    archiveFileName = "${project.name}-${project.version}-custom.jar"

    manifest {
        attributes(
            'Implementation-Title': project.name,
            'Implementation-Version': project.version,
            'Built-Date': new Date().format('yyyy-MM-dd HH:mm:ss')
        )
    }
}

위와 같이 Gradle은 Maven과 Ant의 장점을 합친 빌드 방식입니다.

  • 프로젝트의 구조 표준화
  • 기존 태스크의 커스터마이징 가능
  • Maven과 비교하여 빌드 성능 개선 : 증분 빌드, 병렬 실행, 빌드 캐시

Maven과 비교하여 Gradle의 빌드 성능이 어떻게 개선되었을까?

이전부터 Gradle과 Maven의 빌드 성능을 비교했을 때, Gradle이 압도적으로 빠르다는 것을 익히 들어왔었는데, 빌드 방식의 차이점과 어떻게 구현이 되었는지에 대해 좀 더 자세하게 정리해보려고 합니다.

 

증분 빌드 (Incremental Build)

Gradle의 빠른 빌드의 이유는 첫번째로 증분 빌드 시스템입니다.

Maven은 기본적으로 모듈 단위로 빌드를 수행합니다. 코드 한 줄만 수정해도 해당 모듈 전체를 다시 컴파일하는 방식입니다.

반면 Gradle은 태스크(task) 레벨에서 입력과 출력을 추적합니다.

// Gradle이 자동으로 수행
tasks.named('compileJava') {
    // 1. 소스 파일의 체크섬 계산
    // 2. 이전 빌드와 비교
    // 3. 변경된 파일만 재컴파일
}

실행로그를 확인해보면

> Task :common:compileJava UP-TO-DATE  ← 변경 없음, 스킵!
> Task :api:compileJava                ← 변경됨, 실행
> Task :service:compileJava UP-TO-DATE
> Task :web:compileJava UP-TO-DATE

Gradle은 파일 수준의 변경 사항을 감지하여 실제로 변경된 파일과 그에 의존하는 파일만 재컴파일합니다.

 

빌드 캐시 (Build Cache)

빌드 캐시는 Gradle 3.5부터 도입되었습니다.

# gradle.properties
org.gradle.caching=true

빌드 캐시는 로컨 뿐만 아니라, 원격 캐시도 지원합니다.

이게 무엇을 의미하냐면, 아래 시나리오와 같이 팀원 A가 이미 빌드한 결과물을 팀원 B가 재사용할 수 있습니다.

또한, CI 서버에서 빌드한 결과를 개발자 로컬 머신에서도 활용할 수 있습니다.

개발자 A:
- feature/login 브랜치에서 작업
- ./gradlew build --build-cache
- → 빌드 결과가 원격 캐시에 저장

개발자 B:
- feature/login 브랜치로 체크아웃
- ./gradlew build --build-cache
- → 원격 캐시에서 결과 다운로드
- 컴파일 완전히 스킵! 45초 → 5초 ⚡

Maven도 로컬 리포지토리에 의존성을 캐싱하지만, Gradle의 빌드 캐시는 더 나아가 컴파일 결과물, 테스트 결과 등 모든 태스크 출력을 캐싱합니다.

입력이 동일하다면 캐시된 출력을 재사용하는 방식입니다.

 

데몬 프로세스 (Gradle Daemon)

// gradle.properties
// Gradle 데몬 활성화 (Gradle 3.0부터 기본값)
org.gradle.daemon=true

Gradle 데몬은 백그라운드에서 계속 실행되는 Java 프로세스 입니다. JVM 시작 시간과 클래스 로딩 오버헤드를 제거하여 빌드 시작 시간을 크게 단축시킵니다.

Maven도 mvnd(Maven Daemon)라는 프로젝트가 있지만, Gradle만큼 성숙하지 않고 기본 기능으로 사용하지 않습니다.

Gradle 데몬은 추가적으로 프로젝트 정보, 파일 시스템 상태 등을 메모리에 유지하여 후속 빌드를 더욱 빠르게 만들어줍니다.

 

병렬 실행 (Parallel Execution)

// gradle.properties
org.gradle.parallel=true
org.gradle.workers.max=4

Gradle은 독립적인 프로젝트나 태스크를 병렬로 실행할 수 있어, 멀티 모듈 프로젝트에서 의존성이 없는 모듈들을 동시에 빌드가 가능합니다.

Maven도 -T 옵션으로 병렬 빌드를 지원하지만, Gradle의 태스크 그래프 기반 병렬화가 더 효율적으로 작동합니다. Gradle은 태스크 간 의존성을 정확히 파악하여 최대한 많은 작업을 병렬로 수행합니다.

 

설정 캐싱 (Configuration Cache)

Gradle 6.5에서 도입된 설정 캐싱은 빌드 스크립트 평가 시간을 대폭 줄여줍니다.

// gradle.properties
org.gradle.configuration-cache=true

Gradle의 빌드는 아래 두 단계로 이루어집니다.

  • 설정 단계 (Configuration Phase)
  • 실행 단계 (Execution Phase)

설정 캐싱은 첫 번째 단계의 결과를 저장하여, 빌드 스크립트가 변경되지 않았다면, 이 단계를 완전히 건너뛸 수 있게 합니다.

대규모 프로젝트에서는 설정 단계만 수십초가 걸리는데, 이를 완전히 생략할 수 있다는 것은 엄청난 이점입니다.

 

똑똑한 의존성 관리

Gradle은 동적 의존성 해결에 있어서도 Maven보다 효율적입니다.

configurations.all {
    resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
    resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
}

Gradle은 의존성 메타 데이터를 캐싱하여, 필요한 경우에만 원격 리포지토리를 확인합니다.

또한 의존성 결과도 캐싱하여 동일한 의존성 구조에 대해서는 재계산하지 않습니다.

 

최적화된 Up-to-date Checks

task myTask {
    inputs.files fileTree('src')
    outputs.dir file('build/output')

    doLast {
        // 실제 작업
    }
}

Gradle은 각 태스크에 대해 입력과 출력을 명시적으로 선언할 수 있습니다.

입력이 변경되지 않았다면 해당 태스크는 “UP-TO-DATE”표시되어 실행되지 않습니다.

이 체크는 파일 해시를 기반으로하여 정확하고 빠르게 동작합니다.

 

실제로 얼마나 빠를까?

Gradle 공식 홈페이지에서는 Maven과 Gradle를 사용하여 같은 프로젝트를 빌드했을 때, 성능 차이가 극명하다고 합니다.

  • 클린 빌드: Gradle이 Maven보다 약 2배 빠름
  • 증분 빌드: 상황에 따라 10배에서 100배까지 차이 남

특히 작은 변경사항을 자주 빌드하는 개발 환경에서는 차이를 더 크게 느낄 수 있습니다.

멀티 모듈 프로젝트가 크면 클수록, 빌드를 자주 하면 할 수록 Gradle의 장점이 더 두드러지기 때문입니다.


참고 문서