Compose BOM을 쓰더라도, 최신 버전이 우선적으로 채택된다.
프로젝트에 build-logic 모듈을 적용하여 이전의 복잡한 의존성 관리 체계를 갈아엎었다. 그런데 팀원으로부터 제보가 들어왔다.
런타임 에러가 발생했다는 것.
java.lang.NoSuchMethodError: No static method FlowRow...

NoSuchMethod…? 메서드가 없다고..? 코딩할 때 문제 없었는데 뭔소리??
https://community.intercom.com/mobile-sdks-24/java-lang-nosuchmethoderror-no-static-method-flowrow-9891
다행히 같은 에러를 겪은 사람들이 많았다.
간단히 요약하면 1.8.0 이상의 최신 Compose 버전 사용 시 위 에러가 발생한다는 것...
따라서 해결책은 그저 FlowRow가 포함된 foundation-layout 라이브러리의 버전을 1.8.0 아래 버전으로 낮추면 되는 것이었다.
우선 이게 무엇인지 알아볼 필요가 있었다. 코딩할 때 문제없고, 컴파일도 문제없었는데, 뭐가 문제였을까?
NoSuchMethodError는 말 그대로 "존재하지 않는 메서드를 호출하려고 했을 때" 발생하는 런타임 에러다.
보통 아래의 상황에 발생한다.
컴파일 타임에는 해당 메서드가 있는 줄 알고 성공함. 하지만 실제 앱 실행 시 로딩된 클래스에는 해당 메서드가 없음
즉, 코딩할 때도, 빌드할 때도 문제없이 넘어가지만, 실제 앱이 실행되면서 그 메서드를 찾다가 못 찾으면 터진다.
간단히 생각해서, 내가 코딩할 때 사용한 메서드 (내 경우엔 FlowRow)와 런타임에 채택된 메서드의 형태가 다르다는 것. (또는 메서드가 아예 없어졌거나)
다시 말해, 코딩할 때는 1.8.0 아래 버전의 라이브러리가 사용되다가, 런타임에는 1.8.0 이상의 라이브러리가 사용되었다는 것이다.
아직 뭔지 잘 모르겠어서 확실히 버전을 낮추기 위해 버전 카탈로그를 다시 봤더니 뭔가 이상하다.
build-logic을 적용하면서, 복잡하게 흩어진 버전들을 깔끔하게 관리하기 위해 BOM(Bill Of Materials)을 적용했었다.
[versions]
...
composeBom = "2025.03.01"
[libraries]
...
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } // BOM
androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" }
androidx-compose-animation-core = { group = "androidx.compose.animation", name = "animation-core" }
androidx-compose-animation-graphics = { group = "androidx.compose.animation", name = "animation-graphics" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime-android" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
BOM을 사용하면 버전을 직접 명시하지 않아도 적절한 버전의 조합을 알아서 적용해준다.
처음엔 생각했다. "2025.03.01 BOM 버전이 foundation-layout을 1.8.0 이상으로 제공하는구나!"
그래서 확인해봤더니...?

BOM-2025.03.01 은 foundation-layout 버전을 1.7.8로 제공하고 있었다.
이상하다. 분명 1.8.0 이상에서 발생하는 버그라고 했는데...?
잘 모르겠다. 일단 간단한 거 부터 해보자.
foundation-layout만을 1.7.8 버전으로 고정시키기androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version = "1.7.8" }
composeBom = 2024.02.00
하지만 결과는 같았다.
java.lang.NoSuchMethodError: No static method FlowRow
프로젝트를 한 번 살펴보다가 이런 걸 발견했다.

foundation-layout에 1.8.0-alpha04 버전이 들어가 있었다.

1.8.0 이상 버전이 들어가 있는건지 없는건지... 하던 의심이 확신으로 바뀌는 순간이었다.
왜 내가 넣지도 않은 1.8.0-alpha04 버전이 프로젝트에 포함되어 있는가?
./gradlew :app:dependencies --configuration releaseRuntimeClasspath --info
안드로이드 스튜디오 터미널에서 위 명령어를 실행하면 프로젝트 내의 라이브러리들이 어떤 의존 관계를 가지고 있는지 확인할 수 있다. 그러면 터미널 상에서 아래와 같은 의존성 트리를 보여준다.

나의 목적이었던 foundation-layout을 찾아보면, 다양한 라이브러리들에서 foundation-layout을 의존하고 있다.

위의 이미지 상에서 2개의 foundation-layout을 찾을 수 있었는데, 각각 아래의 사실을 나타냈다.
1. animation-graphics-android(버전 1.8.0-alpha04)가 foundation-layout(버전 1.8.0-alpha04) 를 의존함
2. compose-bom:2025.03.01에 의해 foundation-layout의 버전이 1.7.8로 명시되었으나 실제로는 1.8.0-alpha04 버전이 채택됨
정리하면, foundation-layout은 내가 직접 명시한 것 외에도 다른 라이브러리들에서 의존하고 있고, 최종 버전은 1.8.0-alpha04라는 것이었다. (->기호는 최종적으로 채택된 버전을 나타낸다)
어쩌다 1.8.0-alpha04 버전으로 결정되었을까?
Gradle은 같은 라이브러리가 여러 번 선언되었을 때, 가장 최신 버전을 채택한다고 한다.
즉, 나는 프로젝트 상에서 BOM으로 버전을 결정하고자 하였으나, 다른 라이브러리에 의해 가장 최신 버전인 1.8.0-alpha04가 채택되었던 것이다.
이제 다음 단계는 어떤 라이브러리에 의해 1.8.0-alpha04가 채택되었는지 찾는 것이었다.
위에서 활용한 의존성 트리를 더 살펴보았다.
Compose 라이브러리들을 위주로 의존 관계를 살펴보았고, 최종적으로 이것을 발견할 수 있었다.

네이버 지도를 사용하기 위해 포함시켰던 naver-map-compose 라이브러리가 foundation:1.8.0-alpha04를 의존하고 있었다! (foundation는foundation-layout을 의존하는데, 뒤에 있는 (*) 기호는 앞에 내용과 중복되어 하위 트리 내용이 생략되었음을 의미한다.)
naver-map-compose의 버전을 확인해보고자 깃허브에서 확인해보았다.

naver-map-compose:1.8.0은 Compose 1.8.0 버전을 사용한다고 한다.
결국 naver-map-compose의 버전을 1.8.0으로 명시했던 것이 최종 원인이었다.
build-logic을 적용하면서 네이버 지도 라이브러리 버전을 아무 생각없이 최신 버전으로 올린 것이 문제였던 것이다.
[versions]
...
naverMapCompose="1.7.0"
따라서 네이버 지도의 버전을 낮춰주는 것으로 문제를 해결하였다.
🧑💻 또 다른 방법
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version = { strictly = "1.7.8" } }
strictly 라는 키워드를 이용해서 버전을 강제할 수도 있다고 한다. 이렇게 하면 더 최신 버전이 있더라도 foundation-layout은 1.7.8 버전으로 강제된다.
하지만 다른 라이브러리들과의 관계를 생각했을 때 비교적 덜 안정적인 방식인 것 같다.
BOM만 적용하면 버전은 신경쓸 필요 없다고 생각했는데.... 아니었다.
BOM은 이 프로젝트에서 전혀 활용되지 않고 있었던 것이다.
역시 버전 관리는 중요하다... 버전 막 건드리지 말아야지..