매년 구글 IO를 관심 있게 지켜보는 안드로이드 개발자로서의 소회라면 이런 것입니다. 트렌드세터가 되고 싶다면 꼭 봐야 할 행사, 덕분에 매년 5월 이후면 할 일 목록이 (제 의지와 상관없이 🤦♂️) 풍성해지는 은혜. 2017년, 안드로이드 공식 언어로 코틀린을 채용합니다.와 같은 발표는 실제로 엄청난 영향력을 행사했습니다. 안드로이드는 여전히 자바를 지원하기 때문에 개발 언어로 자바냐? 코틀린이냐?와 같은 양자택일의 상황에 빠질 수 있습니다만 (실제로 막 시작한 개발자들의 단골 질문이기도 하죠?) 자바를 선택한다면 다른 건 차치하고, 시대의 조류를 거슬러야 하는 심리적 저항에 계속 부딪힐 수밖에 없습니다. 이역만리에서 내뱉은 한마디에 심리전까지 해야 하는 안드 개발자의 모진 운명이란. 아무튼 저는 시대의 역풍을 견뎌낼 만한 담대함을 갖고 있지 않기 때문에 구글 IO에 소개된 기술을 곧잘 적용하는 편입니다. 그런데 여기에도 문제가 있습니다.
아마 많은 분이 경험하셨겠지만, 구글의 1.0은 1.0이 아니더라. 서포트 라이브러리 업데이트 후 발견된 이슈로 몇 번의 롤백을 겪고 나서야 얻은 깨달음이라면 버전이 1.0.2쯤 되어서야 ‘아, 이제 좀 쓸만해졌겠구나?’라는 느낌적인 느낌이 사실은 사실이었다는 것을요. 자세한 이야기는 뒤쪽에서 다루겠지만 AndroidX는 시맨틱 버저닝을(https://semver.org/) 엄격하게 적용하고 있습니다.
androidx.car:car:1.0.0-alpha5
androidx.paging:paging-common:2.0.0-rc01
덕분에 개발자는 해당 라이브러리가 현재 어떤 단계에 있는지를 쉽게 알 수 있습니다. 서포트 라이브러리는 24.0.0부터 이런 형태의 버저닝을 부분적으로 사용하기 시작했습니다. 전체 타임라임을 놓고 보면 비교적 최근에 가까운 일이기 때문에 아마 저와 같은 경험을 갖고 있는 피해자(?) 모임을 한다면 꽤 많은 개발자들이 참여하지 않을까 싶습니다.
구글 IO 2018 Android Jetpack: what’s new in Android Support Library 세션에서 서포트 라이브러리는 큰 변화를 앞두고 있었습니다. 왜냐하면 2011년에 등장한 서포트 라이브러리는 28.0.0 버전을 마지막으로 더 이상의 업데이트가 없을 것이라는 일종의 부고 소식을 전했기 때문입니다.
AndroidX: Android Extension Libraries
서포트 라이브러리를 사용하지 않고, 앱을 개발하는 것은 불가능할 정도로 서포트 라이브러리는 안드로이드 개발 생태계의 큰 축이었습니다. 이전 버전의 안드로이드에 대한 하위 호환성 유지, 안드로이드 프레임워크가 제공하지 않는 위젯 및 유틸리티 기능 지원, TV, 차량, 웨어러블과 같은 다양한 폼 팩터에 대응하기 위한 기능을 제공합니다. 하지만 이런 기능적인 측면과 달리 몇 가지 설계적인 한계도 갖고 있습니다.
서포트 라이브러리의 큰 집합 중 하나이자 2011년 최초로 배포된 com.android.support:support-v4를 예로 들면, 첫째로 메이븐 아티팩트와 패키지명이 주는 모호함입니다. support-v4, support-v13이라는 이름에서 v4, v13을 리비전 정보로 착각할 여지가 있습니다. v13이 v4를 포함하면서 더 최신 버전이라고 말이죠. 사실 이 네이밍은 API 레벨 4 이상에서 사용할 수 있는 지원 라이브러리라는 의미입니다. 하지만 minSdkVersion이 19 이상만 되어도 95% 이상을 지원하는 시대 상황을 놓고 보면 support-v4, support-v13, appcompat-v7, cardview-v7과 같은 API 레벨을 의미하는 이름은 시간이 지날수록 퇴색될 수밖에 없습니다. 반전은 support-v4가 더 이상 API 레벨 4를 지원하지 않는다는 것입니다. 원래의 의도와 달리 v4는 24.2.0 배포에서 API 8 이하 지원을 중단했으며(현재는 API 레벨 14 이상만 지원) 동시에 이름이 가지는 규칙성을 상실했습니다. AndroidX 1.0.0은 서포트 라이브러리의 마지막 버전인 28.0.0을 기반으로 새로운 이름과 버전 규칙으로 재정의되었습니다.
두 번째는 모놀리식 형태의 라이브러리 배포입니다. 안드로이드 5.0이 메이저 버전의 OS가 되기 이전에 안드로이드 앱을 개발한 경험이 있다면 DEX 파일의 “64K reference limit” 명세로 인한 적잖은 어려움에 공감하실 테죠. 이는 하나의 DEX 파일에 담을 수 있는 메서드 레퍼런스는 총 65,536개라는 제약에서 비롯됐습니다. Dalvik 런타임은 기본적으로 APK당 하나의 DEX 파일만 로드를 하기 때문에 앱의 규모가 커짐에 따라, 혹은 사용하는 라이브러리 내의 메서드가 많을수록 이런 문제에 더 빨리 직면하게 됩니다.
이런 문제를 일으키는 주범은 Google Play Services였습니다. 6.5 버전부터 라이브러리가 기능 별로 모듈화되었지만 그 이전에는 3만 개에 가까운 함수를 제공하는 단일 라이브러리였습니다. 여기에 support-v4, appcompat-v7를 사용하면 약 8천 개와 1만 2천 개가 더해져서 이 세 라이브러리만으로도 DEX의 한계치인 6.5만의 75% 이상을 차지하는 일이 발생했습니다. ART 런타임 및 멀티덱스 지원 라이브러리가 나오면서 이 문제는 기술적으로 완전히 해결됐지만, support-v4는 이보다 2년 뒤인 24.2.0 버전이 출시되기 전까지 단일 라이브러리로 제공되었습니다.
AndroidX 장점 중 하나는 위젯 단위의 세분화된 형태로 라이브러리를 제공하는 것입니다. 덕분에 메이븐 그룹 ID와 아티팩트 ID는 독립적이고, 직관적으로 정의되었습니다. 예로 서포트 라이브러리에서 ViewPager는 com.android.support:support-core-ui:28.0.0 아티팩트에 포함되어 있습니다. support-core-ui에는 ViewPager뿐만 아니라 DrawerLayout, CursorAdapter, SwipeRefreshLayout, SlidingPaneLayout 등을 포함하고 있고요. 이런 구조로 인해 실제로 사용하지 않는 위젯 꾸러미들을 Gradle 빌드 시스템으로 내려받고, APK에 포함해야만 하는 비효율이 존재했습니다. 반면 AndroidX에서는
androidx.viewpager:viewpager:1.0.0으로 분리되어 원하는 기능만 독립적으로 사용하는 게 가능해졌습니다. 이로 인해 프로가드, 멀티덱스를 사용하지 않는 빌드에서(예를 들면 테스트 빌드) 이전보다 APK를 더 경량화 할 수 있게 됐으며 빌드 속도 면에서도 이득을 볼 수 있습니다.
서포트 라이브러리의 다른 문제점은 바이너리 호환성에 대한 제약입니다. 만약 다음과 같이 27.1.1을 사용하다가 cardview-v7에서 문제가 발생해 27.1.0으로 다운그레이드 하는 경우 appcompat-v7, design도 27.1.0으로 일괄 변경해야만 했습니다. 메이저가 아닌 마이너 버전 업데이트인 경우에도 말이죠.
implementation com.android.support:appcompat-v7:27.1.1
implementation com.android.support:desgin:27.1.1
implementation com.android.support:cardview-v7:27.1.1
각기 다른 버전을 사용하는 경우, 런타임 크래시가 발생할 수 있다는 경고를 볼 수 있기 때문에 이런 실수를 사전에 막을 수는 있지만, 사실 이 문제를 해결하는 최선은 바이너리 간의 호환성을 유지하는 것입니다.
All com.android.support libraries must use the exact same version specification (mixing versions can lead to runtime crashes). Found versions 27.1.1, 27.1.0. Examples include com.android.support:animated-vector-drawable:27.1.1 and com.android.support:cardview-v7:27.1.0
서포트 라이브러리는 패키지 별로 의존성이 높기 때문에 서포트 라이브러리 개발팀은 새로운 버전을 배포할 때마다 의존하는 모든 패키지를 함께 배포하는 형태를 취합니다. 이건 라이브러리를 배포하는 입장에서도 꽤나 성가시고 손이 많이 가는 일입니다. 결론적으로 항상 같은 리비전의 서포트 라이브러리를 사용하는 것이 안드로이드 개발자의 규칙이었습니다. 동시에 서포트 라이브러리 버전 업그레이드가 부담스러운 이유이기도 했고, 혹시나 문제가 발생하게 되면 모든 서포트 라이브러리의 버전을 다운그레이드 해야만 했습니다.
AndroidX는 라이브러리를 사용할 때 개발자가 보편적으로 기대하는 바이너리 호환을 지원합니다. 서포트 라이브러리만의 독자적 버저닝에서 벗어나 X.Y.Z 형태의 시메틱 버저닝을 사용합니다. 따라서 어떠한 코드가 버전 1.4.0에 의존하고 있다면 주 버전(=X)이 같은 1.5.0, 1.8.0과의 호환을 보장합니다.
AndroidX의 바이너리 호환성으로 인해 만일 RecyclerView가 1.0.0에서 1.1.0으로 업데이트됐다면 해당 라이브러리의 버전을 올리는 것만으로 새 버전을 사용할 모든 준비가 끝납니다.
implementation 'androidx.recyclerview:recyclerview:1.1.0'
Jetifier
구글 IO 발표로부터 몇 개월 뒤 2018년 9월, 안드로이드 스튜디오 3.2 정식 버전이 배포되었는데요. 이 버전은 서포트 라이브러리를 사용하는 프로젝트를 AndroidX로 마이그레이션 하기 위한 기능이 내장되었습니다. 마이그레이션은 크게 두 파트로 나뉘는데, 소스코드에서 android.support.*, android.arch.*로 시작하는 네임스페이스를 androidx.*로 치환하는 역할과 빌드 시 의존관계에 있는 서드파티 라이브러리를 바이너리 레벨에서 변환하는 작업입니다.
첫 번째는 소스코드 리팩토링 작업으로 안드로이드 스튜디오의 “Refactor -> Migrate to AndroidX” 메뉴를 통해 작업이 수행됩니다. 프로젝트 볼륨이 작다면 큰 어려움이 없겠지만, 그렇지 않다면 수작업이 필요할 수도 있습니다. 작년 11월 기준으로 몇몇 상황에서 네임스페이스가 완벽히 치환되지 않는 문제가 있기는 하지만 수작업으로 약간의 후처리를 하면 프로덕션 수준에서도 무리 없이 사용할 수 있습니다.(이건 의존하는 라이브러리에 따라 다를 수도 있다는 점) 참고로 이 리팩토링은 java, kotlin, xml, gradle 등 안드로이드 프로젝트에서 사용하는 대부분의 소스코드를 수정하기 때문에 변경사항이 어마할 수 있습니다. 여러 명의 개발자가 여러 브랜치에서 동시에 개발을 하고 있다면 AndroidX 브랜치와 머지 시 필연적으로 발생하는 충돌로 인해 고통에 시달릴 수 있습니다. 이런 상황에 처해있다면 Dan Lew가 작성한 스크립트를 참고해보세요.
위 과정을 통해 소스코드를 androidx.*를 사용하도록 수정했다고 하더라도 빌드는 실패할 수 밖에 없습니다. 왜냐하면 의존관계에 있는 서드파티 라이브러리는 여전히 android.support.*를 사용하고 있기 때문입니다. 이 문제를 해결하기 위해 구글은 Jetifier라는 툴을 배포했습니다. 이 툴은 jar, aar로 배포되는 라이브러리 내의 바이트코드를 빌드 타임에 AndroidX에 대응되도록 변환합니다.
그레이들은 변환된 결과를 캐시로 저장하기 때문에 일반적인 상황에서는 빌드 시간에 큰 영향을 미치지 않지만 캐시가 없는 상태라면 라이브러리 개수와 규모에 따라 전체 빌드 시간이 지연될 수도 있습니다.
Jetifier에 의해 변환된 파일은 $HOME/.gradle/cache/transform/files에서 확인할 수 있습니다. 그림과 같이 폴더 내부에는 Jetified-* 프리픽스가 붙은 라이브러리 목록을 볼 수 있습니다.
변환된 라이브러리의 바이트 코드를 열어보면 Class mappings 규칙에 따라 서포트 라이브러리의 네임스페이스가 AndroidX에 대응되는 형태로 변환된 걸 확인할 수 있습니다.
일례로 위 목록 중 Glide 4.8.0은 자체적으로 AndroidX를 지원하지 않습니다. 내부 코드인 SupportRequestManagerFragment.java는 서포트 라이브러리의 프래그먼트를 사용해 구현되어 있는 것을 확인할 수 있는데요. Jetifier에 의해 변환된 SupportRequestManagerFragment.class 바이트코드를 디컴파일 해보면 다음과 같이 androidx.fragment.app.Fragment를 사용하도록 수정된 것을 볼 수 있습니다. 덧붙여 jeitifer-standalone 버전을 이용하면 AndroidX에서 서포트 라이브러리를 참조하도록 역변환(de-jetification) 하는 것도 가능합니다.
Jetifier의 도움으로 기존 라이브러리들을 AndroidX에서도 문제없이 사용할 수 있지만 종국에는 대부분의 라이브러리들이 AndroidX를 자체적으로 지원하는 형태가 될 가능성이 높습니다. 이미 AirBnb의 DeepLinkDispatch와 같은 라이브러리는 AndroidX 버전을 지원하고 있고, Flutter 진영에서도 플러그인 개발자를 위한 마이그레이션 항목이 명시되어 있는 것을 볼 수 있습니다. Jetifier는 현재와 같은 과도기를 위한 툴이기 때문에 AndroidX를 지원하는 라이브러리들이 안정적으로 정착을 하게 되면 역사 속으로 사라질 운명을 갖고 있습니다.
마치며
서포트 라이브러리는 28.0.0을 끝으로 더 이상 업데이트되지 않기 때문에 Android Q에서는 서포트 라이브러리 29.0.0을 기대할 수 없습니다. 따라서 현존하는 모든 안드로이드 프로젝트는 필연적으로 AndroidX로 마이그레이션 할 수밖에 없는 상황에 놓여있습니다. 앞에서 얘기한 코틀린으로의 전환급은 아니겠지만 코드량의 변화만 놓고보면 격변기라 불러도 손색이 없지 않나 싶어요.
작년 11월(제가 AndroidX로 마이그레이션을 시작한), 당시만 해도 서드파티 라이브러리들의 AndroidX 지원이 한창 진행 중이다보니 (정식 배포 전인) DeepLinkDispatch와 같은 라이브러리를 포크해서 몇 달간 사용하기도 했습니다. 아마 작업을 하면서 마주하시겠지만 유명한 라이브러리들의 깃헙 이슈에 AndroidX와 관련된 글이 빠지지 않고 올라와있는 것을 확인할 수 있습니다. 이런 이슈들의 대다수가 Jetifier와 관련된 빌드 에러인데, 이걸 해결하는 게 마이그레이션 작업의 전부라고 해도 과언이 아닙니다. 아마 지금쯤은, 혹은 앞으로는 AndroidX를 지원하거나 이런 에러들을 수정한 라이브러리들이 많아질 테고, 그래서 상황이 더 좋아질 것으로 예상됩니다.
당근마켓 안드로이드 앱처럼 이미 AndroidX로 이주를 한곳도 있겠지만, Android Q가 공개되는 2019년 중반부터가 본격적인 대이주의 시기가 되지 않을까 싶습니다. 그럼 AndroidX로 안전하게 이주하길 기원하며 이 글을 마칩니다. 🤞