코드를 짜는 시간만큼이나 "빌드가 왜 이렇게 도는가", "이 라이브러리 버전은 대체 누가 정했는가"를 이해하는 게 시니어의 몫이에요. Maven과 Gradle의 철학 차이, 전이 의존성과 버전 충돌 해결 규칙, BOM, 시맨틱 버저닝, 그리고 자바 모듈 시스템(JPMS)까지 — 빌드 로그를 읽어내는 눈을 만듭니다.
빌드 도구가 하는 일은 결국 소스 컴파일 → 의존성 해석 → 테스트 → 패키징(JAR)의 자동화예요. 자바 진영의 양대 산맥인 Maven과 Gradle은 같은 목표를 정반대의 철학으로 풉니다.
| 구분 | Maven | Gradle |
|---|---|---|
| 설정 방식 | 선언적 XML(pom.xml). "무엇을"만 적음 | Groovy/Kotlin DSL(build.gradle). 스크립트로 로직 표현 가능 |
| 실행 모델 | 고정된 수명주기(lifecycle) phase: validate→compile→test→package→install→deploy | 태스크(task) 그래프(DAG). 태스크 간 의존성으로 구성 |
| 철학 | 컨벤션 오버 컨피규레이션 — 표준 구조를 따르면 설정이 거의 없음 | 유연성 — 커스텀 로직·태스크를 자유롭게 정의 |
| 속도 | 기본적으로 매번 다시 수행(증분 약함) | 증분 빌드·빌드 캐시·데몬으로 반복 빌드가 빠름 |
| 확장 | 플러그인(goal 바인딩) | 플러그인 + 임의 태스크 |
mvn package를 치면 그 앞의 validate·compile·test가 자동으로 먼저 실행됩니다. 실제 일은 각 phase에 바인딩된 플러그인 goal(예: compiler:compile, surefire:test)이 해요.
그래서 Maven은 "적게 적고 표준을 따르면 알아서 도는" 도구예요. 대신 표준을 벗어난 커스텀 흐름을 만들기가 상대적으로 뻣뻣합니다.
UP-TO-DATE로 건너뜀), ② 빌드 캐시(같은 입력의 결과물을 재사용 — 로컬·원격 공유 가능), ③ Gradle 데몬(JVM을 살려둬 워밍업 비용 절감). 세 가지가 합쳐져 "바뀐 부분만" 다시 만듭니다. CI에서 원격 캐시를 붙이면 팀 전체가 결과물을 공유해요.라이브러리 하나를 추가하면 그 라이브러리가 쓰는 라이브러리, 또 그게 쓰는 라이브러리가 줄줄이 딸려와요. 이걸 전이 의존성(transitive dependency)이라고 합니다. 빌드 도구가 이 그래프를 자동으로 해석해 클래스패스를 구성해줘요.
spring-boot-dependencies)이에요. 부트 버전만 올리면 수백 개 라이브러리 버전이 검증된 조합으로 함께 맞춰집니다 — 그게 부트 업그레이드의 핵심 가치죠.
<!-- Maven: BOM을 dependencyManagement로 import → 버전 정렬 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 이제 개별 의존성은 version 생략 가능 → BOM이 정한 버전 사용 -->
scope(Maven) / configuration(Gradle)은 "이 의존성이 언제 필요한가"를 정해요. 컴파일에만 필요한지, 런타임에만 필요한지, 테스트에만 필요한지에 따라 최종 산출물에 포함 여부가 달라집니다.
| Maven scope | Gradle configuration | 의미 |
|---|---|---|
compile(기본) | implementation / api | 컴파일·런타임 모두 필요. api는 전이 노출, implementation은 은닉 |
runtime | runtimeOnly | 컴파일엔 불필요, 실행 시에만 필요(예: JDBC 드라이버) |
provided | compileOnly | 컴파일엔 필요하나 런타임은 외부가 제공(예: 서블릿 API) |
test | testImplementation | 테스트에서만 필요. 산출물에 미포함 |
gradle.lockfile(의존성 락킹), Maven 진영의 재현성 도구가 그 예예요. 락파일이 있으면 어제 통과한 빌드가 오늘도 동일한 의존성으로 재현됩니다.전이 의존성을 따라가다 보면 같은 라이브러리의 서로 다른 버전이 그래프 곳곳에서 나타나요. 최종적으로는 하나만 선택돼야 하는데(자바 클래스패스에는 같은 클래스가 하나만 존재), 이때 충돌 해결 규칙이 도구마다 다릅니다.
| 도구 | 기본 충돌 해결 규칙 | 결과 |
|---|---|---|
| Maven | 가장 가까운 정의 우선(nearest-wins) — 의존성 트리에서 루트에 더 가까운(depth 짧은) 버전을 선택 | 버전 번호가 낮아도 가까우면 이김 |
| Gradle | 최신 버전 선택(highest version wins) — 충돌하는 요청 중 가장 높은 버전을 선택 | 깊이와 무관하게 제일 높은 버전이 이김 |
lib:1.0을 직접(depth 1) 쓰고, B(depth 1)가 전이로 lib:2.0(depth 2)을 끌고 온다고 하자.1.0이 더 가까우니 1.0 선택(nearest-wins).dependencyManagement/직접 선언, Gradle은 constraints·resolutionStrategy.force를 씁니다.
NoSuchMethodError가 나는 사고가 흔해요. 진단은 mvn dependency:tree(Gradle은 gradle dependencies)로 트리를 눈으로 확인하는 게 정석입니다.시맨틱 버저닝(SemVer)은 버전을 MAJOR.MINOR.PATCH 세 자리로 나눠 변경의 성격을 전달하는 규약이에요. 예: 3.4.1.
| 자리 | 올리는 경우 | 호환성 |
|---|---|---|
| MAJOR (3.x.x) | 하위 호환이 깨지는(breaking) 변경(API 제거·시그니처 변경) | ❌ 호환 깨짐 — 코드 수정 필요할 수 있음 |
| MINOR (x.4.x) | 하위 호환을 지키며 기능 추가 | ✅ 호환 — 그냥 올려도 됨 |
| PATCH (x.x.1) | 하위 호환되는 버그 수정 | ✅ 호환 — 안전한 업그레이드 |
JPMS(Java Platform Module System, Project Jigsaw, Java 9)는 module-info.java로 모듈의 경계·의존·공개 범위를 명시하게 해요. public이면 어디서든 접근되던 기존 캡슐화의 한계를, 모듈 수준의 강한 캡슐화로 보강합니다.
// module-info.java
module com.example.order {
requires com.example.common; // 이 모듈이 의존하는 모듈
requires java.sql; // 플랫폼 모듈도 명시
exports com.example.order.api; // 외부에 공개할 패키지
// exports 안 한 패키지는 public 클래스라도 모듈 밖에서 접근 불가(강한 캡슐화)
}
| 키워드 | 의미 |
|---|---|
requires | 이 모듈이 필요로 하는 다른 모듈(의존 선언) |
exports | 이 모듈이 외부에 공개하는 패키지. 미공개 패키지는 public이어도 접근 불가 |
requires transitive | 내 모듈을 requires한 쪽에 이 의존을 전이 노출 |
opens | 런타임 리플렉션 접근을 허용(프레임워크용) |
module-info가 있는 JAR를 모듈로 올려, requires로 명시한 의존만 보이고 exports한 패키지만 접근돼요.
모듈패스에 있지만 module-info가 없는 일반 JAR는 자동 모듈(automatic module)로 취급돼 이름이 자동 부여되고 모든 것을 export합니다 — 점진적 이전용 장치예요.
jlink로 커스텀 런타임을 만들 수 있죠), 애플리케이션 레벨의 JPMS 채택은 여전히 낮은 편이에요. 대부분의 팀은 여전히 클래스패스로 빌드·배포하고, 모듈 경계는 빌드 도구의 멀티모듈 프로젝트나 아키텍처 규칙(ArchUnit 등)으로 관리합니다. JPMS는 "존재를 알고 원리를 이해하되, 강제 도입은 신중히"가 실무 감각이에요.재현 가능 빌드(reproducible build)는 "같은 소스·같은 의존성이면 바이트 단위로 동일한 산출물이 나온다"를 목표로 해요. 타임스탬프·파일 순서 같은 비결정 요소를 제거하고, 락파일로 의존성 버전을 고정하며, 빌드 캐시로 동일 입력의 결과를 재사용합니다. 이게 되면 "내 PC에선 되는데" 문제와 공급망 검증이 훨씬 쉬워져요.
SNAPSHOT(개발중, 덮어쓰기)과 릴리스(불변) 저장소를 나눠 운영하는 게 관례입니다.module-info의 requires/exports로 강한 캡슐화. 모듈패스 vs 클래스패스 구분. 앱 레벨 채택은 제한적.