Intro
```
https://en.wikipedia.org/wiki/Dependency_hell
Dependency hell is a colloquial term for the frustration of some software users who have installed software packages which have dependencies on specific versions of other software packages.[1]
The dependency issue arises around shared packages or libraries on which several other packages have dependencies but where they depend on different and incompatible versions of the shared packages. If the shared package or library can only be installed in a single version, the user may need to address the problem by obtaining newer or older versions of the dependent packages. This, in turn, may break other dependencies and push the problem to another set of packages.
```
software engineer의 좌절감을 표현하는... dependency hell. 이 dependency hell은 서로 다른 의존 package에서 shared packages or libraries를 가지고 있으나, 서로 다른 version을 요구하고 있는 상황을 의미한다. 이 현상은 생각보다 software engineering에서 흔하게 발생하는 현상이며 해결이 간단하지 않아 technical debt를 키울 위험성을 높이고, 이에 대한 해결을 회피했다가 software의 잠재적인 위험성을 높인다.
Dependency Resolution Strategy
여러 build tool에서는 이 dependency conflict에 대한 다양한 접근을 한다. 여러 build tool 중 Apache Maven의 경우는 nearest first strategy를 택하고 있으나 이는 완전한 해결책이 될 수 없으며 오히려 version selection 설정에 대한 기회를 박탈한다. Gradle은 이를 개선하여 다양한 상황에서 최선의 version selection을 할 수 있도록 풍부한 표현형을 제공한다. (rich version declaration)
```
dependencies {
implementation('org.slf4j:slf4j-api') {
version {
strictly '[1.7, 1.8['
prefer '1.7.25'
}
}
constraints {
implementation('org.springframework:spring-core') {
version {
require '4.2.9.RELEASE'
reject '4.3.16.RELEASE'
}
}
}
}
```
위와 같이 strict, require, prefer, reject keyword를 이용하여 version selection이 최선으로 선택될 수 있도록 한다.
Generate Dependency Tree
```
./gradlew :gateway:dependencies
+--- project :gateway
| +--- org.springframework.boot:spring-boot-starter-web -> 1.5.9.RELEASE
| | +--- org.springframework.boot:spring-boot-starter:1.5.9.RELEASE
| | | +--- org.springframework.boot:spring-boot:1.5.9.RELEASE
| | | | +--- org.springframework:spring-core:4.3.13.RELEASE
| | | | | \--- commons-logging:commons-logging:1.2
| | | | \--- org.springframework:spring-context:4.3.13.RELEASE
| | | | +--- org.springframework:spring-aop:4.3.13.RELEASE
| | | | | +--- org.springframework:spring-beans:4.3.13.RELEASE
| | | | | | \--- org.springframework:spring-core:4.3.13.RELEASE (*)
```
target module과 이 module이 의존하는 library 간 관계는 어떻게 살펴볼 수 있을까? Gradle에서는 해당 module이 의존하고 있는 library와 또한 해당 library에 대한 중첩 의존성을 한눈에 볼 수 있도록 graph 형태로 제공한다. 이 graph를 통해 의존성 관계를 top-down 방식으로 알아볼 수 있으며 직접 import 하지 않았지만 사용하고 있는 library가 어떤 library로부터 연달아 import 되었는지 알아볼 수 있다. 다른 build tool에서도 이 graph 그리는 기능을 제공하는 것으로 알고 있다.
Investigate Which Library Triggers Un-wanted Version
```
io.netty:netty-resolver-dns:4.1.50.Final -> 4.1.29.Final
```
의존성 관리를 하다보면 위와 같은 상황이 발생할 수 있다. 명시적으로 import한 library에서 4.1.50.Final version을 import 해올 것을 기대했지만 어떤 다른 library에 의해서 downgrade version을 사용하고 있었다. build tool의 transitive dependencies에 대한 resolution이 다소 암묵적으로 발생하기 때문에 (어떤 library들 간에 conflict가 일어나서 우리가 이렇게 해결했어~ blah blah 등의 message는 없다) 위 dependency graph에서는 원인을 찾기가 힘들다. 어떻게 찾을 수 있을까?
```
./gradlew :server:dependencyInsight --dependency netty-resolver-dns --configuration runtime
> Task :server:dependencyInsight
io.netty:netty-resolver-dns:4.1.29.Final (selected by rule)
variant "compile" [
org.gradle.status = release (not requested)
org.gradle.usage = java-api
org.gradle.libraryelements = jar (compatible with: classes)
org.gradle.category = library (not requested)
Requested attributes not found in the selected variant:
org.gradle.dependency.bundling = external
org.gradle.jvm.version = 8
]
io.netty:netty-resolver-dns:4.1.50.Final -> 4.1.29.Final
\--- com.linecorp.armeria:armeria:0.99.7
+--- compileClasspath (requested com.linecorp.armeria:armeria)
+--- com.linecorp.armeria:armeria-retrofit2:0.99.7
| \--- compileClasspath (requested com.linecorp.armeria:armeria-retrofit2)
+--- com.linecorp.centraldogma:centraldogma-client-armeria:0.44.12
| \--- compileClasspath (requested com.linecorp.centraldogma:centraldogma-client-armeria)
+--- com.linecorp.armeria:armeria-brave:0.99.7
```
Gradle에서는 bottom-up 방식의 graph도 제공한다. 즉, peripheral selected library에 대한 의존성을 역으로 표시해주어 해당 의존성의 root를 추적할 수 있도록 한다. 하지만 위 사례의 경우 transitive dependencies를 유발한 다른 library가 드러나지 않는다면?
```
~/.gradle/caches/modules-2/files-2.1
❯ grep -r "4.1.29.Final" *
io.netty/netty-common/4.1.29.Final/bd678341a965ebad75acbf4b36694c7f72440779/netty-common-4.1.29.Final.pom: <version>4.1.29.Final</version>
io.netty/netty-transport-native-epoll/4.1.29.Final/1b0fbfbe4356e8f4193dcb4944cdc1c5154bb11b/netty-transport-native-epoll-4.1.29.Final.pom: <version>4.1.29.Final</version>
...
org.springframework/spring-web/5.0.9.RELEASE/92adb777a0297f0f274fb828a5939c60bbe2a291/spring-web-5.0.9.RELEASE.pom: <version>4.1.29.Final</version>
org.springframework/spring-core/5.0.9.RELEASE/a778b0b1ddb3ee95e599f13f3f35aef9d0cb010d/spring-core-5.0.9.RELEASE.pom: <version>4.1.29.Final</version>
org.springframework.boot/spring-boot-dependencies/2.0.5.RELEASE/b45a4103ccc5577097b9617100d5dee2a461e90d/spring-boot-dependencies-2.0.5.RELEASE.pom: <netty.version>4.1.29.Final</netty.version>
```
pom 파일을 직접 뒤지는 수 밖에~ local repository (Gradle의 경우 caches dir)로 가서 문제가 되는 version을 key로 하여 recursive하게 pom 파일을 읽는다. 그 결과 해당 version의 의존성을 가지는 library를 찾을 수 있었다! 남은 일은 문제가 되는 의존성을 exclude하거나 update 시키는 것이다.
References
https://en.wikipedia.org/wiki/Dependency_hell
https://docs.gradle.org/current/userguide/dependency_resolution.html#sub:resolution-strategy
https://docs.gradle.org/current/userguide/rich_versions.html