Android 앱의 의도치 않은 의존성 변경 방지하기

앱에서 사용하는 라이브러리의 버전을 업데이트하다 보면 의존성이 추가되는 경우가 있는데요. 꼼꼼히 확인하지 않으면 스토어에 배포할 때 혹은 이후에 발견되는 경우가 종종 있습니다. 이런 의도치 않게 변경되는 부분을 사전에 인지할 수 있는 방법을 한 가지 소개합니다.

Sungyong An
7 min readMar 19, 2023

앱을 만들다 보면 여러 가지 부분을 변경하게 되는데요. 코드 스타일에 맞지 않게 작성하는 경우에는 ktlint 같은 도구로, 리팩토링으로 구조를 변경할 때는 단위 테스트 등으로 개발자의 실수를 방지하는 방법이 있습니다.

라이브러리 버전을 올리는 것은 앱 전체에 영향을 주는 큰 변경사항입니다. 여기서는 주의해야 할 것이 없을까요?

문제점

보통 라이브러리 버전을 올리기로 했다면 담당자가 꼼꼼하게 확인할 것 같습니다. 새로운 버전과 이전 버전 사이의 변경점을 확인하고, 바뀐 부분은 적절히 수정해야겠죠.

그런데 라이브러리도 다른 라이브러리에 의존성을 가지고 있는 것들이 많습니다. 예를 들어, 구글의 Jetpack 라이브러리 중에 NavigationFragment에 의존성을 갖고 있어서, Navigation 라이브러리의 버전만 올렸는데 Fragment 버전까지 올라가는 경우를 심심치 않게 볼 수 있습니다.

담당자가 정말 꼼꼼히 확인해서 숨은 참조까지도 변경되는 것을 알았다면 다행이지만, 실제로는 미처 확인하지 못하고 놓치는 경우가 꽤 많았습니다. 롤백하고 처음부터 다시 검토해야 하니까, 추가적인 비용이 발생하고요. 이런 과정이 반복되면 개발자 간에 감정적인 소모가 생길 수도 있습니다.

이런 부분을 사전에 인지할 수 있도록 개선할 수는 없을까요?

Dependency Tree 🌲

Dependency Tree를 출력하는 Gradle Task가 있습니다. 앱에서 사용중인 의존성을 트리 형태로 출력해줍니다. base branch와 working branch 각각 Dependency Tree를 출력해서 서로 비교해보면 되겠습니다.

$ ./gradlew :app:dependencies --configuration releaseRuntimeClasspath

releaseRuntimeClasspath - Runtime classpath of compilation 'release' (target (androidJvm)).
+--- androidx.databinding:viewbinding:7.3.1
| \--- androidx.annotation:annotation:1.0.0 -> 1.4.0
| \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.21 -> 1.7.10
| +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10
| \--- org.jetbrains:annotations:13.0
+--- androidx.databinding:databinding-common:7.3.1
+--- androidx.databinding:databinding-runtime:7.3.1
| +--- androidx.collection:collection:1.0.0 -> 1.1.0
| | \--- androidx.annotation:annotation:1.1.0 -> 1.4.0 (*)
| +--- androidx.databinding:viewbinding:7.3.1 (*)
| \--- androidx.lifecycle:lifecycle-runtime:2.4.0 -> 2.5.1
| +--- androidx.annotation:annotation:1.1.0 -> 1.4.0 (*)
| +--- androidx.arch.core:core-runtime:2.1.0
| | +--- androidx.annotation:annotation:1.1.0 -> 1.4.0 (*)
| | \--- androidx.arch.core:core-common:2.1.0 (*)
| \--- androidx.lifecycle:lifecycle-common:2.5.1
| ...

하지만 매번 눈으로 비교하면 실수할 수 있어서, dependency-tree-diff 혹은 dependency-diff-tldr 같은 라이브러리를 이용하면 조금 더 편합니다.

base branch에 Dependency Tree 결과를 파일로 만들어두면, branch를 전환하지 않고도 의존성이 변경되었는지 확인할 수 있습니다. 당연히 base branch의 Dependency Tree 파일을 적절히 업데이트해둬야겠죠?

그리고 보통 앱을 여러 명이서 개발하니, 매번 직접 실행하기보다는 자동화하는 것이 좋을 것 같습니다.

Dependency Guard 🛡

정확하게 이런 목적으로 만들어진 라이브러리가 있습니다. 바로 2022년도 I/O Extended 행사에서 제가 소개했었던 dependency-guard 입니다. 이걸 이용하면, 의존성 변경을 손쉽게 확인할 수 있습니다. (공짜는 못 참지… 🫠)

(1) 설치하는 방법은 간단합니다.

// build.gradle
buildscript {
dependencies {
classpath "com.dropbox.dependency-guard:dependency-guard:0.3.2"
}
}

// app/build.gradle
apply plugin: 'com.dropbox.dependency-guard'

dependencyGuard {
configuration("releaseRuntimeClasspath")
}

(2) 맨 처음에는 base branch에 의존성 목록을 생성해둡니다.

$ ./gradlew dependencyGuardBaseline

dependencies/${configurationName}.txt 파일이 생성됩니다.

org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10
org.jetbrains.kotlin:kotlin-stdlib:1.6.10
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2
org.jetbrains:annotations:13.0
...

이후에는 의도적으로 변경할 때만 dependencyGuardBaseline를 실행합니다.

(3) 원하는 시점에 의존성이 변경되었는지 확인합니다.

$ # Update a dependency...
$ ./gradlew dependencyGuard

gradle task를 CI / 로컬 빌드에 연계해서 자동화하면 되겠죠? 😃

의존성 변경이 있으면, 아래처럼 오류가 표시됩니다.

Dependencies Changed in :app for configuration releaseRuntimeClasspath
- androidx.browser:browser:1.2.0
+ androidx.browser:browser:1.3.0

앱의 의존성 방지 프로세스가 완성되었습니다! 🎉🎉🎉

오늘은 앱을 개발할 때 자주 발생하는, 의도치 않은 의존성 변경을 방지하는 방법으로 Dependency Guard 라이브러리를 소개해 봤습니다. 이 내용이 앱을 안정적으로 개발하는데 조금이나마 도움이 되었으면 합니다. 🙇

--

--