/ ANDROID, GRADLE, DEPENDENCY, TRANSITIVE

앱의 의존성 확인하고 전이 의존성 변경하기

의존성과 관련하여 문제가 생겼다고 판단되면 의존성 트리를 봐야합니다. 터미널에서 프로젝트가 있는 폴더로 이동하고 아래의 명령어를 실행 하면됩니다.

혹시 {Dementions...}{buildType}부분이 이해되지 않으신다면, ProductFlavors와 BuildType을 참고해주세요.

의존성 트리 보기

명령어 형식

./gradlew app:dependencies --configuration {Dementions...}{buildType}{Compile|Runtime}Classpath   

실제 실행 시켰을때 화면

./gradlew app:dependencies --configuration prodDebugCompileClasspath

> Task :app:dependencies

------------------------------------------------------------
Project :app
------------------------------------------------------------

oneStoreDevDebugCompileClasspath - Compile classpath for compilation 'oneStoreDevDebug' (target  
(androidJvm)).
+--- androidx.core:core-ktx:1.3.2
|    +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.71 -> 1.4.21 (*)
|    +--- androidx.annotation:annotation:1.1.0
|    \--- androidx.core:core:1.3.2
|         +--- androidx.annotation:annotation:1.1.0
|         +--- androidx.lifecycle:lifecycle-runtime:2.0.0 -> 2.1.0
|         |    +--- androidx.lifecycle:lifecycle-common:2.1.0                         # (A)
...
+--- androidx.appcompat:appcompat:1.2.0
|    +--- androidx.annotation:annotation:1.1.0
...
|    |    |    +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
|    |    |    +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
|    |    |    +--- androidx.lifecycle:lifecycle-livedata:2.0.0
|    |    |    |    +--- androidx.arch.core:core-runtime:2.0.0
|    |    |    |    |    +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
|    |    |    |    |    \--- androidx.arch.core:core-common:2.0.0 -> 2.1.0 (*)
|    |    |    |    +--- androidx.lifecycle:lifecycle-livedata-core:2.0.0
|    |    |    |    |    +--- androidx.lifecycle:lifecycle-common:2.0.0 -> 2.1.0 (*)   # (B)
...
+--- org.jetbrains.kotlin:kotlin-stdlib:{strictly 1.4.21} -> 1.4.21 (c)
+--- androidx.core:core-ktx:{strictly 1.3.2} -> 1.3.2 (c)
+--- androidx.appcompat:appcompat:{strictly 1.2.0} -> 1.2.0 (c)
+--- com.google.android.material:material:{strictly 1.2.1} -> 1.2.1 (c)
...
+--- androidx.interpolator:interpolator:{strictly 1.0.0} -> 1.0.0 (c)
+--- androidx.arch.core:core-runtime:{strictly 2.0.0} -> 2.0.0 (c)
\--- androidx.lifecycle:lifecycle-livedata-core:{strictly 2.0.0} -> 2.0.0 (c)

(c) - dependency constraint
(*) - dependencies omitted (listed previously)

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 693ms
1 actionable task: 1 executed

의존성 트리 읽기

(c)는 의존성 제약이 걸려있는 상태를 의미합니다. gradle이 알아서 결정하지 못하도록 명시되어 있는 경우입니다. (\*)은 이전에 트리에서 이미 하위구조를 보여주었기 때문에 생략되었음을 의미합니다.

(B)는 androidx.appcompat:appcompat:1.2.0androidx.lifecycle:lifecycle-common:2.0.0을 참조하고 있지만, (A)에서 androidx.core:core-ktx:1.3.2에서 androidx.lifecycle:lifecycle-common:2.1.0를 참조하고 있기 때문에 최종적으로 2.0.0대신 2.1.0으로 갈음(->)되었음을 알 수 있습니다.

실제 사용 할때는 위 예시 처럼 결과를 화면에 바로 띄우면 사용하기 번거롭습니다. 때문에 아래처럼 파일로 만들거나, 클립보드로 복사해두었다가 내가 쓰기 편한 편집기나 IDE에 붙여놓고 보는 게 편합니다.

파일로 저장하기

./gradlew app:dependencies --configuration prodReleaseCompileClasspath > dependency.tree

클립보드에 복사하기

./gradlew app:dependencies --configuration prodReleaseCompileClasspath | pbcopy  

의존성 문제 해결 하기

의존성을 변경하게 되면 기존 빌드와 다른 환경으로 빌드 되기 때문에 코드 수준의 변경이 일어났다고 생각하는 것이 좋습니다. 빌드가 성공한다고 하여도 런타임에 문제를 일으킬 수 있습니다. 때문에 항상 의존성 관리는 신중해야 하고 변경 후에는 리그레이션 테스트를 통해 검증하는 것이 좋습니다.

실제 예로 보면 눈이 너무 아프니 간단한 상황을 만들고 간략하게 예를 만들어 진행해 보겠습니다.

시나리오 1 (전이 의존성 컨트롤하기:upgrade)

dependencies {                                                                                  
    implementation 'B:1.0.0'
}
./gradlew app:dependencies --configuration compileClasspath                                      
...
+--- B:1.0.0
     \--- C:1.0.0

내 프로젝트에서는 C를 직접 사용지 않았습니다. 하지만 C:1.0.0에 의존성을 가진 B:1.0.0이 있습니다. 그런데 C:1.0.0에 문제가 있어서 B를 사용할때 특정상황에서 버그가 있습니다. 찾아보니 C:1.2.0버전에서는 이 문제를 해결했습니다. 하지만 B는 아직 업데이트 되지 않았습니다. 결국 B에서 C의 버전만 올리면 되는 상황입니다.

참고로 이런 상황에서 B에 대한 명시적인 의존성을 가진 직접 의존성(direct dependencies)이라고 하고 C에대한 의존성은 전이 의존성(transitive dependencies)이라고 합니다.

전이 의존성을 변경하기 위해서는 아래처럼 constraint블럭을 지정하고 직접 기입해주면 됩니다. because에 명시적으로 이유까지 적어주면 좋겠습니다.

dependencies {                                                                                  
    implementation 'B:1.0.0'
    constraints {
        implementation('C:1.2.0') {
            because 'previous versions have a bug impacting this application'
        }
    }

}
./gradlew app:dependencies --configuration compileClasspath
...
+--- B:1.0.0
|    \--- C:1.0.0 -> 1.2.0 (*)

시나리오 2 (전이 의존성 제외하기 : downgrade)

여러 모듈 의존성을 가진 X가 있습니다. 물론 그중에 최신 버전을 사용하는 모듈도 포함되어 있습니다. 그런데 X의 최신 버전이 내 코드에서 문제를 일으키는 것을 발견하게 되었습니다. 이런 경우 강제로 버전을 다운그레이드 합니다. 방법은 특정 버전을 strictly로 지정하면 됩니다.

dependencies {                                                                                  
    implementation 'X'
    {
        version {
            strictly '1.0.0'
        }
    }
    implementation 'A:1.0.0'
    implementation 'B:1.1.0'
    implementation 'C:1.0.1'
}

또는 dependencies 블록 밖에서 전역적으로 덮어 쓸 수도 있습니다.

configurations.all {                                                                            
    resolutionStrategy.force "X:1.0.0"
}
./gradlew app:dependencies --configuration compileClasspath                                     
...
+--- X:{strictly 1.0.0} -> 1.0.0
+--- B:1.0.0
|    \--- X:1.3.0 -> 1.0.0 (*)
+--- C:1.1.0
|    \--- X:1.4.0 -> 1.0.0 (*)
+--- D:1.0.1
     \--- X:1.5.0 -> 1.0.0 (*)

시나리오 3 (전이 의존성 제외하기 : exclude)

가장 많이 게 되는 상황입니다. 특정 라이브러리를 가져와서 사용해야겪 하는데, 특정 모듈이나 그룹을 제외하고 싶습니다. 가령 aar로 가져온 모듈과 gradle로 가져온 모듈이 서로 다른버전을 보고 있는 경우가 있습니다. 또는 버전에 따라 패키지 형상이 다른 라이브러리에 각각 의존성이 있는경우가 이에 해당합니다. 이럴 때는 gradle로 가져오는 모듈에서 제외 할 수 있다면 그 부분만 제거합니다.

dependencies {                                                                                  
    implementation('X:1.0.0') {
        exclude group: '{group_name}', module: '{module_name}'
    }
}

Search

Get more post