Java 프로젝트에서 Maven과 Gradle을 비교하고, 의존성 충돌을 진단/해결하는 방법과 멀티 모듈 구성의 핵심만 빠르게 정리합니다.
도입 (문제 상황)
라이브러리 버전 하나 올렸을 뿐인데 런타임에서 NoSuchMethodError가 터지거나, 로컬에선 되는데 CI에서만 빌드가 깨진 경험 있으실 거예요. 원인은 대개 “의존성 그래프가 생각보다 복잡해졌다”는 데 있습니다. Maven/Gradle을 단순 빌드 도구가 아니라 의존성 해석기로 이해하면 문제 해결 속도가 확 달라집니다.
핵심 개념: Maven vs Gradle, 그리고 “의존성 그래프”가 중요한 이유

의존성 관리는 결국 (1) 어떤 버전을 선택할지와 (2) 충돌이 나면 무엇을 기준으로 이길지를 정하는 일입니다. Maven과 Gradle은 둘 다 “전이 의존성(transitive dependency)”을 따라가며 그래프를 만들고, 규칙에 따라 최종 클래스패스(또는 모듈패스)를 결정합니다. 여기서 규칙 차이가 곧 장애의 차이로 이어집니다.
Maven & Gradle 선택 가이드(실무 관점)
| 항목 | Maven | Gradle |
|---|---|---|
| 설정 방식 | 선언적 XML(POM) 중심 | DSL(Kotlin/Groovy) + 태스크 기반 |
| 의존성 버전 충돌 기본 규칙 | Nearest wins(가까운 경로 우선) | 기본은 최신 버전 선택 경향(conflict resolution) + 강력한 제어 수단 |
| 멀티 모듈 | 오래된 표준, 안정적 | 유연하고 확장성 높음(플러그인/컨벤션) |
| 빌드 성능 | 안정적이나 상대적으로 느릴 수 있음 | 캐시/증분 빌드/병렬화로 유리한 경우 많음 |
| “팀 표준화” | 기업 레거시/표준 많음 | 최근 신규 프로젝트에서 선호 증가 |
중요한 포인트는 “무조건 Gradle이 낫다/무조건 Maven이 낫다”가 아니라, 문제가 났을 때 의존성 그래프를 얼마나 빨리 설명하고 고칠 수 있느냐입니다. 빌드 스크립트 문법보다, 다음 두 가지가 더 중요합니다.
- 버전의 단일 출처(Single Source of Truth): BOM/플랫폼/상위 POM으로 버전을 한 곳에서 관리
- 그래프 가시화: 어떤 라이브러리가 어떤 경로로 들어왔는지 즉시 확인
의존성 충돌이 실제로 터지는 방식
대표적인 증상은 아래처럼 “컴파일은 되는데 런타임에서 깨지는” 형태입니다.
NoSuchMethodError,ClassNotFoundException- 로그 구현체 충돌(SLF4J 바인딩 중복)
- Jackson/Netty/Guava 같은 핵심 라이브러리 버전 뒤틀림
- Spring Boot 스타터와 개별 의존성 버전 혼용
flowchart TD
A["Your App"] --> B["Library A"]
A["Your App"] --> C["Library B"]
B["Library A"] --> D["Common Lib v1"]
C["Library B"] --> E["Common Lib v2"]
D["Common Lib v1"] --> F["Runtime Classpath"]
E["Common Lib v2"] --> F["Runtime Classpath"]
의존성 그래프에서 같은 라이브러리의 서로 다른 버전이 만나면 “최종 선택 규칙”에 따라 한 버전만 남고, 그 결과가 런타임 오류로 이어질 수 있다는 흐름도입니다.
코드 예제: Maven/Gradle에서 충돌 진단 + 해결 + 멀티 모듈 기초
아래 예제는 “복붙해서 바로 실행”을 목표로 한 Gradle(Kotlin DSL) 멀티 모듈 샘플입니다.
app모듈은 실행 가능한 애플리케이션lib모듈은 공용 라이브러리- BOM(플랫폼)로 버전을 고정하고
- 충돌 진단을 위한
dependencyInsight를 같이 사용합니다.
실행 환경: Java 17, Gradle 8.x
프로젝트 구조
dependency-demo/
settings.gradle.kts
build.gradle.kts
app/build.gradle.kts
app/src/main/java/com/example/App.java
lib/build.gradle.kts
lib/src/main/java/com/example/lib/Lib.java
settings.gradle.kts
rootProject.name = "dependency-demo"
include("app", "lib")
루트 build.gradle.kts (버전 단일 출처 + 공통 설정)
plugins {
// 루트에는 적용하지 않고 버전만 정해두는 방식
id("java") apply false
}
allprojects {
repositories {
mavenCentral()
}
}
subprojects {
apply(plugin = "java")
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
// 핵심: 버전은 한 곳(플랫폼/BOM)에서 통제
dependencies {
// Jackson BOM: Jackson 관련 모듈 버전을 일관되게 맞춤
add("implementation", platform("com.fasterxml.jackson:jackson-bom:2.17.1"))
}
}
lib/build.gradle.kts (라이브러리 모듈)
dependencies {
// 버전은 BOM이 결정하므로 여기서는 버전을 쓰지 않음
implementation("com.fasterxml.jackson.core:jackson-databind")
}
lib/src/main/java/com/example/lib/Lib.java
package com.example.lib;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Lib {
public static String toJson(Object value) throws Exception {
// Jackson 버전이 흔들리면 런타임 에러가 나기 쉬운 지점
return new ObjectMapper().writeValueAsString(value);
}
}
app/build.gradle.kts (애플리케이션 모듈 + 충돌 예시/해결)
dependencies {
implementation(project(":lib"))
// 일부러 jackson-core를 다른 버전으로 끼워 넣으면 충돌이 생길 수 있음
// (실무에서는 "직접 추가한 의존성"이 스타터/BOM과 어긋나며 자주 발생)
implementation("com.fasterxml.jackson.core:jackson-core:2.13.5")
// 해결 1) 강제로 특정 버전 고정 (권장: BOM/플랫폼으로 맞추고, 강제는 최후 수단)
configurations.all {
resolutionStrategy {
force("com.fasterxml.jackson.core:jackson-core:2.17.1")
}
}
}
app/src/main/java/com/example/App.java
package com.example;
import com.example.lib.Lib;
import java.util.Map;
public class App {
public static void main(String[] args) throws Exception {
String json = Lib.toJson(Map.of("hello", "dependency"));
System.out.println(json);
}
}
실행/진단 명령어
# 실행
./gradlew :app:run
# 의존성 트리 확인 (어떤 경로로 들어오는지)
./gradlew :app:dependencies --configuration runtimeClasspath
# 특정 모듈의 버전이 왜 선택됐는지(충돌 원인/결과)
./gradlew :app:dependencyInsight --dependency jackson-core --configuration runtimeClasspath
Maven에서도 동일한 진단이 가능합니다.
mvn dependency:tree,mvn dependency:tree -Dincludes=...같은 방식으로 “어떤 경로로 들어왔는지”를 먼저 확인하시면 됩니다.
실무 팁
💡 실무에서는: “버전은 선언하지 말고, 관리만 하세요”
- Spring Boot를 쓰신다면 **Boot BOM(Dependency Management)**을 기준으로 버전을 맞추는 게 가장 안전합니다.
- Boot 스타터를 쓰면서 개별 라이브러리 버전을 여기저기서 직접 박기 시작하면, 언젠가 반드시 충돌이 납니다.
- 팀 규칙으로 “직접 버전 선언은 예외 케이스만 허용” 정도로 잡아두면 장애가 크게 줄어듭니다.
💡 실무에서는: 충돌 해결 순서를 고정해 두면 빨라집니다
dependencyInsight/dependency:tree로 유입 경로를 찾기- 가능하면 상위 BOM/플랫폼에서 정렬하기(가장 권장)
- 불가피하면
exclude로 원치 않는 전이 의존성을 제거 - 그래도 안 되면
force/constraints로 고정(최후 수단)
특히force는 “당장 빌드는 통과”시키지만, 다른 모듈에서 더 미묘한 런타임 문제를 만들 수 있어 변경 이력을 남기고(왜 강제했는지) 주기적으로 제거하는 게 좋습니다.
의존성 관리는 “추가”보다 “정렬”이 더 중요합니다. Maven/Gradle 중 무엇을 쓰든, 버전의 단일 출처와 그래프 진단 루틴만 갖추면 충돌 대응이 훨씬 쉬워집니다.
멀티 모듈은 공통 코드를 나누는 목적도 있지만, 더 큰 가치는 의존성 정책을 중앙에서 통제할 수 있다는 점입니다.
핵심 요약
- Maven/Gradle은 빌드 도구이면서 “의존성 그래프 해석기”입니다.
- 충돌은 트리(경로)부터 보고, BOM/플랫폼으로 버전을 정렬하는 게 정석입니다.
- 멀티 모듈은 공통 설정(리포지토리/자바 버전/BOM)을 중앙에서 관리하기 좋습니다.
다음 글: [#37 클린 코드 실천 가이드]
'JAVA' 카테고리의 다른 글
| Java 코딩 컨벤션 정리: Google/Oracle 스타일 가이드 비교와 팀 컨벤션 만드는 법 (2) | 2026.03.03 |
|---|---|
| Java 클린 코드 실천 가이드: 네이밍부터 코드 리뷰 체크리스트까지 (0) | 2026.03.02 |
| Java 성능 체크리스트: String 연결부터 메모리 누수 패턴까지 (0) | 2026.03.01 |
| Java 단위 테스트 시작하기: JUnit 5 기초와 Given-When-Then 패턴 (0) | 2026.03.01 |
| Java 효과적인 로깅 전략: SLF4J + Logback, 로그 레벨 가이드, 안티패턴 정리 (0) | 2026.02.28 |