Theme:

build.gradle 파일은 매일 보는데, 이게 정확히 뭘 하는 건지 설명할 수 있는가? dependencies에 뭔가를 추가하면 라이브러리가 알아서 들어오고, ./gradlew build를 치면 jar 파일이 뚝 떨어진다. 편리하지만, 그 안에서 무슨 일이 일어나는지 모른 채 쓰고 있었다면 이 글이 도움이 될 것이다.

TIP 이 글의 코드 예제를 직접 실행해보고 싶다면 Java 기본기 핸드북을 확인해보세요.

빌드 도구가 왜 필요한가

코드를 작성하고 실행하기까지는 생각보다 많은 단계가 있다.

PLAINTEXT
[소스 코드] → 컴파일 → 테스트 → 패키징 → 배포

         외부 라이브러리 다운로드 & 클래스패스 설정

작은 프로젝트라면 javac로 컴파일하고 java로 실행하면 된다. 하지만 프로젝트가 커지면 문제가 생긴다.

  • 의존성 관리: Spring Boot를 쓰려면 수십 개의 jar가 필요하다. 하나하나 다운로드할 것인가?
  • 테스트 자동화: 코드를 바꿀 때마다 테스트를 수동으로 돌릴 것인가?
  • 패키징: jar, war 같은 배포 파일을 매번 수작업으로 만들 것인가?
  • 환경 일관성: 내 PC에서는 되는데 팀원 PC에서는 안 된다면?

빌드 도구는 이 모든 과정을 자동화한다. Java 생태계에서 주로 쓰이는 빌드 도구는 두 가지다.

도구등장 시기설정 파일설정 방식
Maven2004년pom.xmlXML 선언형
Gradle2012년build.gradle / build.gradle.ktsGroovy/Kotlin DSL

역사적으로 Ant → Maven → Gradle 순서로 발전해왔다. Ant는 자유도가 높지만 표준이 없었고, Maven이 "컨벤션 오버 컨피규레이션"을 도입해 표준을 만들었다. Gradle은 Maven의 장점을 취하면서 더 유연하고 빠른 빌드를 제공한다.

Maven 기본 — pom.xml 구조

Maven 프로젝트의 핵심은 pom.xml(Project Object Model) 파일이다.

XML
<?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에서도 동일하다.

PLAINTEXT
my-app/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/          ← 소스 코드
│   │   └── resources/     ← 설정 파일
│   └── test/
│       ├── java/          ← 테스트 코드
│       └── resources/     ← 테스트용 설정 파일
└── target/                ← 빌드 결과물 (자동 생성)

Maven 라이프사이클

Maven의 가장 큰 특징은 표준 빌드 라이프사이클이다. 빌드 과정을 단계(phase)로 나누고 순서대로 실행한다.

PLAINTEXT
validate → compile → test → package → verify → install → deploy
단계하는 일
validate프로젝트 설정이 올바른지 검증
compilesrc/main/java 소스를 컴파일
test단위 테스트 실행
package컴파일된 코드를 jar/war로 패키징
install패키지를 로컬 저장소(~/.m2)에 설치
deploy패키지를 원격 저장소에 배포

핵심은 누적 실행이다. mvn package를 실행하면 validate → compile → test → package가 모두 실행된다.

BASH
# 실무에서 자주 쓰는 명령어
mvn compile                    # 컴파일만
mvn test                       # 테스트까지
mvn clean package              # 깨끗하게 빌드 후 패키징 (가장 많이 씀)
mvn clean package -DskipTests  # 테스트 건너뛰기 (급할 때만)

Maven 의존성 관리

scope — 의존성의 사용 범위

scope컴파일테스트런타임패키징대표 예시
compile (기본)OOOOspring-core
testXOXXJUnit, Mockito
providedOOXXServlet API
runtimeXOOOJDBC 드라이버

공부하다 보니 provided가 좀 헷갈렸다. 컴파일할 때는 필요하지만, 실행 환경(톰캣 등)이 이미 갖고 있으니 jar에 안 넣는다는 뜻이다. war 배포할 때 Servlet API가 이중으로 들어가면 충돌이 나기 때문에 이 구분이 중요하다.

전이 의존성 (Transitive Dependency)

A가 B를 의존하고, B가 C를 의존하면 — A는 자동으로 C도 가져온다.

PLAINTEXT
my-app → spring-boot-starter-web → spring-webmvc → spring-core

편리하지만, 내가 추가하지 않은 라이브러리가 들어오면서 버전 충돌이 발생할 수 있다. mvn dependency:tree로 의존성 트리를 확인하는 습관이 중요하다.

Gradle 기본 — build.gradle 구조

Gradle은 Groovy DSL 또는 Kotlin DSL로 빌드 스크립트를 작성한다. XML보다 간결하고, 조건문이나 반복문도 쓸 수 있다.

GROOVY
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를 선호하는 추세다.

같은 의존성을 두 도구로 비교하면 간결함의 차이가 확연하다.

XML
<!-- Maven: 5줄 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.2.0</version>
</dependency>
GROOVY
// Gradle: 1줄
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.0'

Gradle 태스크와 의존성

의존성 설정(Configuration)

Gradle에서는 Maven의 <scope>에 해당하는 개념을 configuration이라고 부른다.

Gradle 설정Maven scope 대응설명
implementationcompile컴파일 + 런타임, 소비자에게 노출 안 됨
apicompile컴파일 + 런타임, 소비자에게 노출됨
compileOnlyprovided컴파일에만 사용
runtimeOnlyruntime런타임에만 사용
testImplementationtest테스트 컴파일 + 런타임에 사용

implementation vs api는 면접에서도 종종 나오는 포인트다.

PLAINTEXT
모듈 A (library)
├── api: commons-lang3         → 모듈 B에서도 사용 가능
└── implementation: guava      → 모듈 B에서 사용 불가

모듈 B (application)
└── implementation: 모듈 A

implementation으로 선언하면 의존성이 모듈 내부에 캡슐화된다. 컴파일 속도도 빨라지고, 내부 구현을 바꿔도 소비자 모듈에 영향이 없다. 특별한 이유가 없다면 implementation을 기본으로 쓰자.

Gradle 태스크

BASH
# 자주 쓰는 명령어
./gradlew build        # 컴파일 + 테스트 + jar 생성
./gradlew clean build  # 깨끗하게 다시 빌드
./gradlew bootRun      # Spring Boot 실행
./gradlew dependencies # 의존성 트리 확인

커스텀 태스크도 만들 수 있다. Maven에서는 플러그인을 만들어야 하는 일이 Gradle에서는 몇 줄이면 된다.

GROOVY
// 커스텀 태스크 정의
tasks.register('hello') {
    doLast {
        println '안녕하세요, Gradle!'
    }
}

의존성 충돌 해결

라이브러리 A가 guava 31.0을 요구하고, B가 guava 30.0을 요구한다면? Maven과 Gradle은 서로 다른 전략으로 해결한다.

Maven: Nearest-First

의존성 트리에서 루트에 가장 가까운 버전을 선택한다. 같은 깊이면 먼저 선언된 쪽이 이긴다.

PLAINTEXT
my-app
├── A (guava 31.0)        ← 깊이 1: 선택됨
└── B → C (guava 30.0)    ← 깊이 2: 무시됨

Gradle: Newest-First

가장 높은 버전을 선택한다. 대부분 상위 버전이 하위 호환되므로 더 안전한 편이다.

충돌을 직접 해결하는 방법

1. exclude — 특정 전이 의존성 제거

GROOVY
// Gradle
implementation('com.example:library-a:1.0.0') {
    exclude group: 'com.google.guava', module: 'guava'
}
XML
<!-- 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 덕분이다.

GROOVY
// Gradle: BOM 사용
dependencies {
    implementation platform('org.springframework.boot:spring-boot-dependencies:3.2.0')
    implementation 'org.springframework.boot:spring-boot-starter-web'  // 버전 생략 가능
}

3. 버전 강제 지정

GROOVY
// Gradle: 특정 버전을 강제
configurations.all {
    resolutionStrategy {
        force 'com.google.guava:guava:31.1-jre'
    }
}

Maven vs Gradle 비교

비교 항목MavenGradle
설정 파일pom.xml (XML)build.gradle (Groovy/Kotlin DSL)
빌드 속도느림 (캐시 없음)빠름 (증분 빌드, 빌드 캐시)
의존성 충돌nearest-firstnewest-first
커스터마이징플러그인에 의존태스크로 자유롭게 작성
러닝 커브낮음 (표준이 명확)중간 (DSL 학습 필요)
빌드 캐시없음로컬 + 원격 캐시 지원
데몬 프로세스없음Gradle Daemon (JVM 재사용)

Gradle이 빠른 이유는 세 가지다.

  1. 증분 빌드: 변경된 파일만 다시 컴파일한다
  2. 빌드 캐시: 이전 빌드 결과를 캐시해두고, 입력이 같으면 재사용한다
  3. Gradle Daemon: JVM을 백그라운드에 띄워두고 재사용한다

대규모 프로젝트에서 Gradle이 Maven보다 2~10배 빠른 것으로 알려져 있다. Spring Framework 자체도 Maven에서 Gradle로 전환했다.

어떤 걸 선택해야 하는가? 새 프로젝트라면 Gradle을 추천한다. 기존 Maven 프로젝트를 굳이 전환할 필요는 없다. 중요한 것은 도구가 아니라 의존성 관리와 빌드 프로세스를 이해하는 것이다.

Multi-module 프로젝트

프로젝트가 커지면 모듈을 분리해야 한다.

PLAINTEXT
my-project/
├── settings.gradle          ← 모듈 목록 정의
├── build.gradle             ← 루트 (공통 설정)
├── core/                    ← 공통 모듈 (엔티티, 유틸)
│   ├── build.gradle
│   └── src/
├── api/                     ← API 서버 모듈
│   ├── build.gradle
│   └── src/
└── batch/                   ← 배치 모듈
    ├── build.gradle
    └── src/
GROOVY
// settings.gradle — 모듈 등록
rootProject.name = 'my-project'
include 'core', 'api', 'batch'
GROOVY
// 루트 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'
    }
}
GROOVY
// 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을 설치하지 않아도 빌드할 수 있게 하는 도구다.

PLAINTEXT
my-project/
├── gradlew              ← Linux/Mac용 실행 스크립트
├── gradlew.bat          ← Windows용 실행 스크립트
└── gradle/wrapper/
    ├── gradle-wrapper.jar
    └── gradle-wrapper.properties   ← Gradle 버전 정보
PROPERTIES
# gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip

왜 중요한가:

  1. 설치 불필요: ./gradlew만 있으면 지정된 Gradle을 자동 다운로드한다
  2. 버전 일관성: 모든 개발자와 CI 서버에서 동일한 버전으로 빌드한다
  3. 재현 가능한 빌드: 6개월 후에 클론받아도 같은 환경에서 빌드된다

실무에서는 gradle 명령어 대신 항상 ./gradlew를 사용하는 것이 원칙이다.

BASH
# 버전 업그레이드도 간단하다
./gradlew wrapper --gradle-version 8.6

Maven에도 Maven Wrapper(mvnw)가 있다. 같은 원리로 동작한다.

정리 테이블

개념MavenGradle
설정 파일pom.xmlbuild.gradle / .kts
의존성 식별GAV 좌표동일 (group:name:version)
의존성 저장소~/.m2/repository~/.gradle/caches
빌드 단위Phase (라이프사이클)Task
의존성 범위scopeconfiguration
의존성 충돌nearest-firstnewest-first
버전 통합 관리<dependencyManagement>platform()
멀티모듈부모 POM + <modules>settings.gradle + subprojects
Wrappermvnwgradlew
증분 빌드 / 캐시미지원지원

면접 포인트 요약

  • 빌드 도구의 역할: 컴파일, 의존성 관리, 테스트, 패키징을 하나의 명령으로 처리
  • 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편 시리즈의 마무리로, 지금까지 다룬 내용을 어떤 순서로 학습하면 좋을지 정리해볼 예정이다.

댓글 로딩 중...