본문으로 건너뛰기

Maven의 Transitive Dependency 길들이기

· 약 6분

Maven으로 의존성을 관리하다보면 라이브러리 버전이 꼬이는 경우가 종종 있습니다. 그동안은 주먹구구식으로 해결하곤 했는데 한번쯤 확실히 알아둬야겠다고 생각해서 정리해 보았습니다.

Transitive Dependency란?

어떤 아티팩트를 의존성으로 추가하면, 그 아티팩트가 가지고 있는 의존성이 함께 딸려옵니다. 그렇게 '딸려온' 의존성을 Transitive Dependency라고 합니다.

아래 의존 관계 트리에서 MyProject A이고 A X이므로 MyProject X의 의존 관계가 생겼습니다.

  • MyProject
    • A
      • X

(참고사항) 의존 관계 디버그

Maven Dependency Plugin을 사용하면 의존 관계 트리를 찍어볼 수 있습니다.

$ mvn dependency:tree
[INFO] [dependency:tree]
[INFO] org.apache.maven.plugins:maven-dependency-plugin:maven-plugin:2.0-alpha-5-SNAPSHOT
[INFO] \- org.apache.maven.doxia:doxia-site-renderer:jar:1.0-alpha-8:compile
[INFO] \- org.codehaus.plexus:plexus-velocity:jar:1.1.3:compile
[INFO] \- velocity:velocity:jar:1.4:compile

또는 IntelliJ에 있는 기능을 사용할 수도 있습니다. (근데 Ultimate Edition에서만 되는 듯 합니다.)

의존 관계 중재 (Dependency Mediation)

의존 관계 트리에 한 아티팩트의 여러 버전이 있으면 어떤 버전이 선택될까요? 가장 가까운 정의가 선택됩니다.

  • MyProject
    • A
      • X 1.0
    • B
      • C
        • X 2.0

위의 트리에서, MyProject 기준으로 X 1.0이 X 2.0보다 가까이 있습니다. 따라서 X 1.0이 선택됩니다.

만약 거리(깊이)가 같으면 어떻게 될까요?

  • MyProject
    • A
      • X 1.0
    • C
      • X 2.0

이 때는 먼저 선언된 쪽이 이깁니다. A가 C보다 먼저 선언되었으므로 X 1.0이 선택됩니다.

원하는 버전으로 고정하기

의존성 사이에 충돌이 일어났을 때 어떤 알고리즘으로 중재되는지 살펴봤습니다. 이제 충돌을 우리가 원하는 버전으로 해결하는 방법을 알아보겠습니다.

방법 1: 직접 의존성으로 포함

MyProject에 원하는 버전의 아티팩트를 직접 포함시키면, 이것이 가장 가까운 의존성이 되므로 항상 선택됩니다.

  • MyProject
    • A
      • X 1.0
    • B
      • C
        • X 2.0
    • X 2.0

하지만 MyProject의 코드에서 X를 직접 사용하지 않는다면 불필요한 의존성을 추가한 것이므로 좋은 방법이 아닐 수 있습니다.

방법 2: 원하지 않는 의존성 제외

<exclusion> 설정을 이용하면 원하지 않는 의존성을 제외할 수 있습니다. 다음과 같이 A에서 X를 제외하면 X 2.0이 가장 가까운 의존성이 되어 선택됩니다.

  • MyProject
    • A
      • X 1.0
    • B
      • C
        • X 2.0

이 방법의 단점은 X 1.0에 의존하는 아티팩트가 여러개라면 일일히 제외시켜줘야 한다는 것입니다. 그리고 의존성 버전을 바꾸게 될 때마다 기존에 의도한 버전이 계속 선택되고 있는지 확인해야 합니다.

방법 3: Dependency Management 설정 사용

<dependencyManagement> 설정으로 특정 아티팩트의 딸려온 의존성을 포함한 모든 의존성의 버전을 고정할 수 있습니다.

POM에 다음 내용을 추가하면 모든 X가 기존에 설정된 버전을 무시하고 2.0 버전으로 고정됩니다.

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>X</artifactId>
<version>2.0</version>
</dependency>
</dependencies>
</dependencyManagement>
  • MyProject
    • A
      • X 1.0 **X 2.0**
    • B
      • C
        • X 2.0

Dependency Management 활용: BOM

규모가 큰 라이브러리는 여러 모듈로 쪼개져서 배포되는 경우가 있습니다. 예를 들어 Jackson은 jackson-core, jackson-databind, jackson-dataformat-yaml 등의 모듈로 나눠져 있습니다.

보통은 문제가 안되지만, 이렇게 나눠진 모듈끼리 버전이 안 맞으면 공포의 ClassNotFoundException을 유발하는 원인이 됩니다. 예를 들어 jackson-core는 2.8인데 jackson-databind는 2.6이라거나요.

그래서 이렇게 쪼개진 라이브러리들은 대부분 "bill of materials" (BOM)을 함께 배포합니다. BOM을 임포트하면 해당 라이브러리의 모든 모듈을 특정 버전으로 고정할 수 있습니다.

다음 내용을 POM에 추가하면 모든 Jackson의 모듈이 2.9.0 버전으로 강제됩니다.

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.9.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

레퍼런스