더 작은 APK를 위한 Android App Bundle에 대해서 🐷

한로니
당근 테크 블로그
20 min readAug 5, 2019

시작하는 글

2018년 구글 IO The future of apps on Android and Google Play 세션에서 앱 사이즈와 설치율에 대한 상관관계가 눈길을 끌었습니다. 이 세션은 앱의 사이즈가 커질수록 플레이스토어의 앱 설치 전환율이 낮아진다는 이야기로 시작을 했는데요. APK 크기가 6MB씩 증가할 때마다 사용자가 앱을 설치하는 비율(방문자 대비 설치율, 전환율)이 1%씩 떨어진다는 것입니다.

Conversion rate decreases as apps get lager — https://youtu.be/0raqVydJmNE

반대로 APK의 사이즈를 줄이는 것만으로도 낮아진 전환율을 되돌릴 수 있다는 연관성에 대해 언급하며 안드로이드 앱 번들을 소개했습니다. 이번 포스트에서는 차세대 퍼블리싱 포맷인 안드로이드 앱 번들에 대한 개괄, 당근마켓에서 앱 번들로 전환하며 직면했던 이슈와 해결책에 대해 이야기합니다.

APK의 크기는 왜 증가하는가?

구글에 의하면 APK의 평균 크기는 2012년 이래로 다섯 배 이상 커졌습니다.

Average APK size over time — https://youtu.be/0raqVydJmNE

안드로이드가 지향하는 개방형 환경 덕분에 안드로이드 앱은 다양한 플랫폼에서 실행됩니다. 이는 각기 다른 CPU가 탑재된 여러 기기를 지원한다는 의미이기도 합니다. 네이티브 코드를 사용하는 APK의 lib 폴더에는 ABI 별로 공유 라이브러리가(.so) 위치해있습니다. 안드로이드 빌드 시스템의 기본 동작은 팻 바이너리(뚱뚱한 이진 파일 🐷)인 단일 APK를 생성하도록 구성되어있습니다. 각 ABI에 대응하는 공유 라이브러리가 APK에 중복된 형태로 존재하는 기술적 이유이기도 하죠. 유니버설 APK라고도 불리는 이런 형태는 배포가 용이하다는 장점이 있습니다. QA 팀에 APK를 전달하는 상황을 상상해보면 개발자는 테스트로 사용하는 기기의 아키텍처가 무엇인지? 화면 밀도는 어떻게 되는지에 대해 전혀 고민할 필요가 없습니다. 왜냐하면 이 APK는 안드로이드가 구동되는 많은 기기에서 실행될 수 있도록 런타임에 필요한 모든 구성이 이미 포함된 상태이기 때문입니다. 단점은 단일 아키텍처를 지원하는 APK보다 크기가 커진다는 데 있습니다. 게다가 새로운 기기를 지원하거나 고해상도화 되는 하드웨어를 위해 고화질 리소스의 추가, 다국어 지원을 위한 새로운 언어셋의 추가로 인해 시간이 지날수록 유니버설 APK 크기는 자연 증가할 수밖에 없는 구조를 갖고 있습니다. 특히 이터레이션마다 새로운 기능이 추가되는 게 일상다반사인 모바일 비즈니스의 특성상 APK 크기의 증가는, 그래서 더 놀랍거나 새로울만한 일은 아닙니다.

APK 크기는 왜 중요한가?

Google I/O 19에서 구글은 이런 결과를 발표했습니다.

Three reasons why apps fail to install — https://youtu.be/rEuwVWpYBOY

플레이 스토어에서 다운로드 버튼을 누른 사용자 중 70%만이 앱 설치에 성공을 한다는 것입니다. 앱 설치에 성공하지 못한 30%의 사용자를 분석한 결과 주요 원인을 Space, Time, Cost라고 도출했습니다.

Space

20억이 넘는 기기를 확인한 결과 이중 28%는 1GB 미만의 여유 공간을 갖고 있는 것으로 파악됐습니다. 플레이 스토어는 기기에 충분한 저장 공간이 없을 때, 앱을 설치하는 대신 언인스톨 위자드를 실행합니다. 따라서 이런 사용자는 앱 설치에 어려움을 겪을 가능성이 높습니다.

Time

APK의 사이즈가 클수록 설치 완료에 이르기까지 더 많은 시간이 소요되기 때문에 사용자가 다운로드 취소 버튼을 클릭할 확률이 높아집니다. 또한 다운로드 중 음영 지역으로 이동하는 등의 네트워크 이슈에 의한 에러로 설치 전환율이 낮아지기도 합니다.

Cost

와이파이, 무제한 데이터 요금제가 당연시 여겨지는 지역도 있겠지만 대다수의 사용자들에게 데이터 요금은 중요한 문제입니다. 이런 사용자들은 본인의 잔여 데이터가 얼마나 남았는지 확인 후 다운로드 중인 앱을 취소할 가능성이 높습니다.

특히 이런 현상은 저사양 기기가 많이 보급되어 있고, 네트워크 인프라가 열악한 이머징 마켓에서 뚜렷하게 나타납니다. 하지만 저장 공간이 부족한 현상은 북미, 유럽 등에서도 동일하게 관측되고 있기 때문에 특정 국가에 국한된 문제만은 아닙니다. 불행히도 APK 크기는 설치 전환율에만 영향을 미치는 게 아닙니다. 기기에 설치된 앱이 삭제되는 요인으로도 꼽힙니다. 구글이 미국에서 실시한 사용자 조사에 의하면 삭제된 앱 다섯 개 중 하나는 저장 공간을 확보하기 위함이라고 하는데요. 흥미로운 것은 하루 안에 앱을 삭제하는 주된 이유는 앱의 퀄리티, 한 달 뒤에 삭제하는 이유는 저장 공간 때문이라고 합니다. 또한 기기의 저장용량이 부족할 때 구글 플레이는 사용자에게 제거할 앱을 추천하는데, 이때 기기에 설치된 앱의 크기를 고려해서 제안을 합니다. 따라서 APK 크기는 앱의 설치율과 유지율에 관심 있는 누군가에게는 파볼만 한 가치가 있는 영역이라 할 수 있습니다.

다중 APK

안드로이드는 팻 바이너리가 아닌 기기에 특화된 APK를 생성할 수 있는 기능을 오래전부터 지원해왔습니다. 다중 APK라 불리는 이 기술은 몇몇 상황에서 꽤 유용하게 사용할 수 있습니다. 대표적인 사례는 안드로이드 하위 버전에 대해 신규 기능을 제공하지는 않지만 버그 패치가 필요한 경우, 즉 API 레벨 16~19를 대상으로 하는 APK와 API 레벨 21 이상을 대상으로 하는 APK를 분리해서 플레이 스토어에 올리는 경우입니다. 한 앱에 대해 두 개의 APK를 플레이 스토어에 업로드하는 방식이지만 사용자는 기기의 OS 버전에 해당하는 APK만 설치(또는 업데이트) 하기 때문에 사용자 경험이 달라지지는 않습니다.

비슷한 방식으로 아키텍처와 화면 크기, 밀도에 따라서 APK를 최적화할 수도 있습니다. 이 경우 APK에서 중복된 ABI 바이너리와 화면 밀도별 리소스들이 제거되는데, 이런 이유로 앱 번들이 소개되기 이전에 네트워크와 저장 공간의 낭비를 줄일 수 있는 대표적인 방법이었습니다.

Multiple APKs — https://youtu.be/0raqVydJmNE

하지만 크기를 줄이기 위해 다중 APK를 사용하는 건 놀랍게도 비효율적이고 손이 많이 가는 작업입니다. 모든 APK를 직접 버저닝 하고, 관리해야 하기 때문에 분리된 APK 수가 많아질수록 더 많은 시간과 노력을 기울일 수밖에 없었습니다. 배포 단계가 개발 사이클에서 새로운 병목이 될 가능성이 높기 때문에 무작정 모든 아키텍처와 화면 밀도에 최적화된 APK를 만들 수 없는 노릇이었던 거죠. 또한 기술적 한계가 존재했는데 애초에 변경될 일이 없는 ABI, 화면 밀도와 달리 언어는 기기 설정에서 언제든 바뀔 수 있기 때문에 지원하는 모든 언어셋을 APK에 반드시 포함시켜야만 했습니다.

앱 번들

2017년 구글 플레이 앱 서명 기능이 출시되면서 배포와 관련된 몇 가지 변화가 생기기 시작했습니다. 이전에는 배포를 위해서 반드시 필요했던 앱 서명이 개발자 사이드에서 이루어졌습니다. 배포 준비가 된 APK를 플레이 콘솔로 업로드하면, 구글 플레이는 APK를 사용자 기기로 서빙하는 역할이 전부였습니다. 하지만 구글 플레이 앱 서명은 개발자를 대신해 APK에 최종 앱 서명을 하는데 이 사소한 변화가 구글이 APK 최적화에 관여할 수 있는 계기가 됩니다. 바로 앱 서명 직전의 틈을 이용해, APK를 최적화하는 것이죠. 따라서 구글 앱 서명을 활성화하면 다중 APK를 생성, 업로드, 관리하는 성가신 작업을 직접 할 필요가 없습니다. 하나의 유니버설 APK를 업로드하는 것만으로도 구글 인프라를 통해 다중 APK 생성과 업로드가 자동으로 수행되는 혜택을 받을 수 있습니다.

안드로이드는 롤리팝을 기점으로 분할 APK(Split APKs)라는 기법을 플랫폼에 적용합니다. 이때까지만 해도 하나의 앱은 하나의 APK로 구성된다.라는 게 참이었습니다. 사용자가 앱을 설치하는 것은 플레이 스토어에서 하나의 APK를 내려받았다는 의미로 여겨졌는데, 다중 APK가 적용된 앱이라고 해도 다르지 않았습니다. 플레이 스토어에 업로드 된 여러 APK 중 사용자 기기에 최적화된 하나의 APK를 받는 형태가 되니까요. 그런데 분할 APK는 이 패러다임을 바꿔 놓습니다. 하나의 앱은 여러 개의 APK로도 구성될 수 있다.라고 말입니다.

Base APK with configuration APKs — https://youtu.be/0raqVydJmNE

구글이 18년 IO에서 발표한 안드로이드 앱 번들을 구성하는 기술적 토대가 바로 여기에 있습니다. 앱 번들이 도입되기 전 개발자는 플레이 콘솔에 APK 파일을 업로드했습니다. 하지만 앱 번들을 활성화하면 APK가 아닌 AAB(파일 확장자 .aab)라 불리는 앱 번들 파일을 업로드해야 합니다. AAB는 구글 플레이 앱 게시(업로드)를 위한 파일 포맷이고, 이 파일을 기반으로 아키텍처, 화면 밀도, 언어에 최적화된 분할 APK를 생성합니다. 최종적으로 앱 실행에 필수 요소인 base APK와 아키텍처, 화면 밀도, 언어별로 구분된 configuration APKs, 모듈식의 분리된 기능을 위한 APK인 dynamic feature APKs가 생성됩니다. 앱을 구성하는 각 APK들이 사용자 기기에 여러 번 설치되면서 하나의 앱이 구성되는 셈입니다. 앱 번들은 이를 다이내믹 딜리버리라고 소개합니다.

예를 들어 앱을 다운로드 한 사용자 기기의 아키텍처가 armeabi-v7a이고 화면 밀도는 xxhdpi, 언어는 fr이라고 한다면 구글 플레이는 base-master.apk, base-armeabi_v7a.apk, base-xxhdpi.apk, base-fr.apk를 기기로 전달합니다. 분할 APK를 이용해 기기에 최적화된 조합을 만드는 게 가능해진 것입니다.

An example of Dynamic Delivery serving just what’s needed to a device — https://developers-kr.googleblog.com/2018/05/google-io-2018-whats-new-in-android.html

특히 다이내믹 딜리버리의 멋진 기능이랄 만한 부분은 분할 APK들을 한 번에 설치할 수 있을 뿐만 아니라 지연 설치가 가능하다는 점입니다. 모든 언어셋을 포함해야만 했던 다중 APK의 제약을 해결할 실마리인 셈이기도 하죠. 앱을 다운로드하는 시점에는 언어가 ko였지만 사용자가 기기 설정에서 언어를 es로 바꾼다면 플레이 스토어는 base-es.apk를 자동으로 내려받습니다. 이 기술은 앱 번들의 기능 중 하나인 동적 기능(dynamic feature)에도 사용됩니다. 앱에서 소규모 사용자를 위한 기능이나, 자주 사용하지 않는 부가 기능을 동적 기능으로 분리하면 앱 내에서 사용자가 이 기능을 필요로 할 때, 해당 모듈의 apk를 온디맨드로 받거나 조건에 따라 내려받는 게 가능합니다. 또한 해당 기능이 더 이상 필요하지 않을 때 삭제가 가능하기 때문에 기기의 저장 공간을 효율적으로 관리할 수 있습니다.

앱 번들을 활성화하면 현재 (또는 미래에 추가될) 구글 플레이가 수행하는 최적화 기능을 사용할 수 있습니다. 그중 하나가 마시멜로에 소개된 extractNativeLibs=”false”을 이용한 네이티브 라이브러리를 압축하지 않은 상태로(Uncompressed native library) APK를 생성하는 옵션입니다.

Uncompressed native libraries — https://youtu.be/QdoEcfibG-s

마시멜로 이전에는 APK 내부에 네이티브 라이브러리가 압축된 형태로 존재했기 때문에 플랫폼이 네이티브 라이브러리를 로드하려면 인스톨 시점에 압축된 라이브러리를 해제해야만 했습니다. 한마디로 기기 내 설치된 앱의 크기가 커진다는 의미였죠. 하지만 마시멜로부터는 기술적으로 APK에서 압축되지 않은 네이티브 라이브러리를 바로 로드할 수 있게 됐습니다. 이 방식은 내부 공간을 절약하는 효과도 있지만 업데이트 시 패치의 크기를 줄여주는 효과도 있습니다. 왜냐하면 패치를 생성하는 알고리즘이 압축되지 않은 파일에서 더 나은 성능을 보이기 때문입니다.

당근마켓에서

TMI로 앱 번들에 대해서 지금껏 얘기를 했지만 시작은 조금 사소했습니다. 안드로이드 개발자 카이가 플레이 콘솔에 뜬 경고인지 알림인지를 보고, 이번 업데이트는 앱 번들로 출시를 해야겠다.라고 마음먹은 걸 시작으로 앱 번들이라는 신문물이 흘러 들어오게 됐으니까요. 사실 당근마켓 앱의 다운로드 크기는 동종 앱 대비 준수한 편에 속해서(10MB 중반) 최적화에 많은 기대는 없었습니다. 오히려 이 글을 작성하면서 자료를 정리하다 보니 뒤늦게 결과가 꽤 괜찮다는 걸 알게 된 셈입니다. 당근마켓은 한 달 전부터 앱 번들을 이용해 업데이트를 출시하고 있습니다. 아래 그래프에서 앱 번들 이전과 이후의 다운로드 크기 추이를 볼 수 있는데요.

지난 3개월간 당근마켓 앱 다운로드 크기 그래프

앱 번들 적용 후 다운로드 크기는 14MB에서 9MB로 36%가 줄어들었습니다. 기기에 설치된 앱의 크기 또한 변화가 있었는데 68MB에서 54MB로 21%가 감소했습니다. 구글이 선전하는 다음 그림이 과장이 아님을 확인한 셈이기도 합니다.

App download size savings — https://youtu.be/flhib2krW7U

사이드로드와의 역학관계에 대해서

앱 번들 적용 후 당근마켓 안드로이드 괴발자들은 행복하게 살았습니다.라고 이야기가 끝날 줄 알았다면 오산이라며 Crashlytics에 이런 이슈가 수집됩니다. 😢

Caused by android.view.InflateException Binary XML file line #39: Binary XML file line #39: You must supply a layout_width attribute.

눈을 비비고 다시 봐도 이 메시지를 있는 그대로 받아들이기가 힘들었습니다. 혼란스러움에 개발의 근간이 흔들릴 참이었거든요. 코드는 두말할 것 없이 layout_width가 잘 작성되어 있었습니다. 혹여나 실수로 이 끔찍한 버전이 배포가 됐다면 엄청난 양의 크래시가 보고됐을 텐데, 이 에러는 수십 명 정도의 소규모 그룹에서만 발생했습니다. 앱 번들 적용 후에 발생한 이슈니까 원인도 거기에 있겠구나.라는 유추는 비교적 쉽게 할 수 있었는데요. 하지만 언제 발생하는지에 대한 단서는 쉽게 떠오르지 않았습니다. 이런저런 시간 뒤에 알게 된 사실은 바로 사이드로드였습니다. 왜인지는 모르겠지만 몇몇 사용자들은 구글 플레이 스토어에서 당근마켓 앱을 설치하지 않고, 인터넷상에 돌아다니는 APK를 설치하고 있었습니다. 만일 이 APK가 유니버설 APK였다면 문제없이 실행이 됐을 테지만 앱 번들로부터 생성된 앱의 APK라면 곤란한 상황이 발생하게 됩니다.

앱 번들은 여러 APK들의 조합으로 하나의 앱이 구성된다는 걸 기억하실 거에요. 그럼 인터넷상에 돌아다니는, 누군가의 기기에서 추출됐을 APK의 정체는 뭘까요? 앱 번들을 통해 설치된 앱은 하나의 base APK와 여러 개의 configuration APKs, dynamic feature APKs로 구성됩니다. 몇몇 사용자가 사이드로드로 설치한 APK는 이중 base APK입니다. base-master.apk는 앱 실행에 필요한 필수 요소들을 포함하고 있기 때문에 이 APK만으로도 앱을 구동할 수 있습니다. 하지만 앱 내의 어떤 액티비티가 뷰의 크기를 화면 밀도 별로 정의해뒀다면 여기서부터 문제가 발생합니다.

안드로이드 빌드 시스템은 res/values/의 하위에 정의된 XML 파일들을 resources.arsc로 컴파일 합니다. 이 파일은 문자열, 스타일, 밀도와 같은 정보뿐만 아니라 resource.arsc에 직접 포함되지 않는 레이아웃 및 이미지와 같은 APK 내에 존재하는 콘텐츠의 경로도 갖고 있습니다. 앱 번들 이전의 APK에서는 resource.arsc에 정의된 모든 정보는 동일한 APK 내에 존재했습니다. 하지만 앱 번들의 base-master.apk에서는 참조에 해당하는 실제 값이 base APK가 아닌 configuration APKs에 존재하는 경우가 발생합니다. 왜냐하면 앱 번들은 화면 밀도에 따라서 base-[mdpi|hdpi|xhdpi|xxhdpi|xxxhdpi].apk를 각각 생성하기 때문입니다. 구글 플레이는 다이내믹 딜리버리를 통해 기기에 필요한 모든 APK를 제공하지만 사이드로드는 그렇지 않습니다. 따라서 xxhdpi 기기를 소유한 사용자가 사이드로드로 당근마켓의 base-master.apk만 설치했다면 base-xxhdpi.apk에 존재하는 값을 참조하는 시점에 크래시가 발생합니다.

이 문제를 해결하는 가장 간단한 방법은 화면 밀도로 APK를 분리하는 옵션을 비활성화하는 것입니다. 앱 번들은 다음과 같은 설정을 제공하기 때문에 리소스를 base-master.apk에 통합시켜 생성할 수 있습니다. 소스 코드 수정 없이 해결할 수 있는 방법이지만 앱 번들이 지향하는 가치와는 조금 거리가 있습니다.

bundle { 
abi.enableSplit = true
density.enableSplit = false
language.enableSplit = true
}

두 번째 방법은 런타임에 사이드로드로 설치된 앱인지를 체크해서, 만일 참이라면 사용자에게 플레이 스토어를 통해 앱을 설치하라고 안내하는 방법입니다.

class MyCustomApplication : Application {
override fun onCreate() {
if (MissingSplitsManagerFactory.create(this).disableAppIfMissingRequiredSplits()) {
// Skip app initialization.
return
}
super.onCreate()
...
}
}

다음과 같은 팝업을 띄워서 앱 실행에 필요한 요소들이 제대로 설치되지 않았음을 사용자에게 안내하는 것이죠.

An alert for sideloaded app — https://developer.android.com/guide/app-bundle/sideload-check

.aab 파일은 설치를 위한 포맷이 아니기 때문에 adb install을 이용해 앱을 디바이스로 직접 설치할 수 없습니다. 하지만 사이드로드 된 앱이 어떻게 동작하는지 로컬에서 테스트하고 싶은 경우 .aab로부터 기기에 최적화 된 분할 APK들을 생성할 필요가 생깁니다. 즉, 기기에 base-master.apk만 설치했을 때 실제로 앱이 어떻게 동작하는지를 테스트하기 위함이죠. 커맨드 라인 툴인 bundletool을 이용하면 이 목적을 쉽게 달성할 수 있습니다.

bundletool build-apks — connected-device — bundle=/MyApp/my_app.aab — output=/MyApp/my_app.apks

앱 번들을 사용하면 이전에 없던 사이드로드로 인한 이슈가 생길 가능성이 아주 높습니다. 화면 밀도뿐만 아니라 네이티브 라이브러리를 사용하는 경우도 주의를 기울일 필요가 있습니다. 실제 당근마켓 앱에서는 서버로 이미지 업로드를 위한 전처리 작업에 네이티브 라이브러리를 사용하는데 사이드로드 된 앱에서 .so를 로드하지 못해 업로드 기능이 동작하지 않는 현상도 있었습니다. 그런데 너무 걱정할 필요는 없는 게, 실제 사이드로드한 사용자는 아주 미미한 수준이었습니다. WAU 170만 중 100명이 채 되지 않았거든요. 이건 앱이 서비스되는 국가마다 수치가 다를 수 있기 때문에 일반화하기는 어렵습니다. 다만 전에 없던 크래시 이슈가 수집된다고 해도 너무 걱정 마시길.

마치는 글

지난 7년간 APK의 크기는 다섯 배 이상 커졌고, 이 수치는 앞으로도 계속 증가할 것으로 예상됩니다. 구글은 몇 년 전 인스턴트 앱을 통해 사용자가 앱을 발견하고 즉시 실행할 수 있는 새로운 사용자 경험에 대해 이야기했습니다. 앱 번들은 그 연장선상에서 Modular, instant, and dynamic이라는 구글이 제시하는 앱의 청사진을 완성해 보였습니다. 이 기술을 관통하는 하나의 요소를 뽑자면 단연 모듈화입니다. 하나의 모듈로 하나의 안드로이드 앱을 만드는 방식은 이제 시대에 뒤처진 방법론이 될 가능성이 더 높아졌습니다. 모듈화된 안드로이드 프로젝트가 주류로 확산되는 데 앱 번들이 부스트를 달아버린 셈이죠. 그런 의미에서 단일 모듈로 만들어진 앱을 앱 번들로 전환했다는 건 이제 막 왕좌의 게임 시즌1을 끝낸 기분이랄까요? 어서 다이내믹 피처를 적용해보고 싶은 저는 앱 번들 시즌2로 돌아오겠습니다. 긴 글 읽어주셔서 감사합니다.

읽어볼 만한 글

--

--

한로니
당근 테크 블로그

컴퓨터 앞에 앉아있는 시간이 많지만, 어째서인지 자전거를 타고 세계 곳곳을 여행하는 상상을 종종하는 괴발자 🤞