본문으로 바로가기
반응형

Photo from unsplash

App bundle이 적용되면서 앱 설치 시 설치 대상 기기에 맞춤 사항들이 조합되어 Google play를 통해 다운로드됩니다.

기본 동작을 하는 Base APK, 해상도와 관련된 Desity APK, Native Libaray APK, 언어 APKs 등이 Play store에서 각기 분리된 Split Apk로  존재하면서 설치 요청 시 단말에 필요한 것들만 조합되어 다운로드 및 설치됩니다.

이렇게 Google Play가 자동으로 분할되어 존재하는 APK 이외에 앱 개발자가 기획적이거나 기술적인 이유로 특정 기능을 분리하여 필요에 따라 설치/삭제할 수 있도록 만든 것을 Dynamic Feature Module이라고 합니다.

동적 다운로드를 크게 두 개의 분류로 나눠보면 특정 기능(동작 코드 + Native library + Resource)을 분리하여 다운로드하는 Feature Delivery와 게임에서 흔히 사용하는 추가 리소스를 다운로드 하는 Asset Delivery로 구분할 수 있습니다. 여기서는 Asset Delivery는 논외로 다루지 않습니다. 필요한 경우 Google Developer 페이지에서 확인할 수 있습니다 [2]

Feature Module Type

앱이 App bundle을 이용한 Multi Apks로 구성됨에 따라서 개발시점에 특정한 기능을 분리된 Module로 만들어 배포할 수 있습니다. App bundle에 포함되어 마켓에 배포되면, 아래 옵션에 따라 설치되도록 조정할 수 있습니다. [1]

1. Install-time delivery

기본 기능을 제공하는 Base APK와 Configuration APKs의 설치와 함께 같이 설치됩니다. "어차피 install 되면서 설치되는 부분을 왜 분할하여 Module로 만들지?"라는 의문을 가질 수 있으나, Module로 기능을 분리할 경우 삭제가 가능합니다. 따라서 설치 시에는 필요하나, 추후 필요없는 기능이라면 이 형태로 설치를 진행합니다.

예를 들어 신규 기능에 대한 튜토리얼을 Feature Module로 분리하여 만든다면, 설치 시 같이 포함되도록 하고, 사용자가 튜토리얼을 보고 나면 삭제하여 설치 공간을 줄일 수 있습니다.

2. On demand delivery 

필요한 시점에 Google Play에 요청하여 기능을 다운로드하여 설치합니다.

예를 들어 옷을 파는 쇼핑앱이 있다고 가정합니다. 대부분의 사람들은 이 앱을 통해서 옷을 구매하지만, 이 앱에는 본인의 옷을 올려서 중고로 판매하는 기능도 가지고 있다고 할 때, 이 기능을 이용하는 사용자에게만 중고 거래 기능과 관련된 부분을 제공할 수 있습니다.

3. Conditional delivery

특정 하드웨어와 관련되거나, min/max sdk에 따라 기능 제공, 설치된 국가에 따른 조건을 가지고 설치 여부를 정할 수 있습니다. 하드웨어적으로 지원되는 경우에만 AR 기능을 다운로드 받게 하거나, 최소 요구 API 이상에서만, 특정 국가에서만 다운로드 받도록 조건을 설정할 수 있습니다.

4. Instant delivery

Instant app은 App bundle 이전에도 존재했던(2017년부터 제공) 기능으로 apk를 설치하지 않고도 실행할 수 있도록 합니다.

App Modulization

구글에서 언급하고 있는 앱을 모듈화 하면서 얻을 수 있는 장점은 아래와 같습니다.

1. 동시 개발

앱을 논리적인 구성 요소로 분리하여 개발하여 동시 개발실 병합과 충돌을 줄일 수 있습니다.

2. 빌드 시간 단축

Module로 분리된 기능들을 사용하면 Android Studio에서 gradle 빌드시 병렬 처리로 인하여 빌드 시간이 줄어듭니다.

3. 기능 제공 맞춤 설정

기능을 Module로 분리하여 모듈화하고 설치 여부를 조정할 수 있기 때문에 이에 따른 이점이 발생합니다.

제가 "구글에서 언급한 장점이다"라고 기술한 이유는 위에서 언급된 세 가지 장점이 크게 와 닿지는 않기 때문입니다. 기능을 분리함으로써 얻는 설치 공간 save라는 달콤한 사탕이 있지만, 동시 개발되더라도 모듈별, Base와 모듈 간 (호출) 관계가 발생하며, 공용으로 사용되는 libray들에 대해서는 gradle dependency가 implements에서 api로 변경되어야 하는 사항들은 빌드 속도를 감소시킵니다. 각 모듈별로 따로 library들을 정의해 준다면 implements를 유지할 수 있겠지만 lib 버전 변경 시 여러 곳에 퍼져있는 버전 관리가 쉽지 않습니다.

무엇보다도 기존 기능을 무리하게 리팩터링 하여 분리한다면 말 그대로 분리된 모듈로 인한 예기치 못한 crash나 이슈가 발생할 수도 있습니다. 이 부분은 추후 기능 모듈 마지막 포스팅에서 정리합니다.

 

Feature 모듈 사용 시 주의사항

  • 다량의 Feature Module 사용: Conditional / On demand 형태로 50개 이상의 모듈을 설치 시 성능 문제가 발생할 수 있습니다.
  • Install-time deliver 개수 제한: 해당 구성 모듈을 10개 이하로 제한하도록 합니다. 그렇지 않으면 앱 다운로드 및 설치 시간이 증가합니다.
  • L OS 5.0(API 21) 이하 기기: 4.4 kikat처럼 L OS 이전 OS에서는 Multi-apks를 지원하지 않습니다. 추후에 나올 fusing 옵션을 이용하여 universal APK를 생성하도록 해야 합니다.
  • android:exported=true Activity에서 사용: 외부에서 호출 가능한 Activity에서는 기능 모듈 access시 주의해야 합니다. 이는 모듈이 아직 다운로드되지 않은 상태 일수 있기 때문에 문제를 일으킬 수 있습니다.
  • SplitCompat 사용: On demand로 기능 모듈의 동적 다운로드 시 기본 app이 다운로드한 module에 접근할 수 있도록 합니다.

 

Feature Module 생성

실제로 안드로이드 스튜디오에서 Dynamic Feature 모듈 생성은 아래 절차와 같이 간단하게 생성이 가능합니다.

1. New Module을 생성합니다.

2. Dynamic Feature Module을 선택합니다.

3. Moudle name을 넣습니다. pacakge name 또한 설정해야 하며, 기본적으로 기본 package경로가 표시됩니다.

4. 이름과 Title을 넣습니다.

- Module title은 필요시 유저에게 보이는 이름입니다. 따라서 이 내용은 생성 후 자동으로 strings.xml에 표기됩니다.

- install-time inclusion은 모듈을 언제 설치에 대한 값을 선택합니다. install-time, on-demand, conditional install 중에 하나를 고릅니다. (저는 on-demand로 선택했습니다.)

5. 마지막으로 Fusing 여부를 선택합니다.

Dynamic module은 LOS 이상에서만 동작하므로 4.4 Kitkat처럼 하위 버전에서 동작하게 하려면 install시 모든 모듈을 포함하는 universal apk를 설치해야 합니다. 이렇게 universal apk를 생성할지에(fusing) 대한 여부로 단말 tarket이 5.0 이하라면 check 해 줍니다.

마지막으로 모듈이 추가된 것을 볼 수 있습니다.

(저는 newfeature1, newfeature2 두 개를 추가했습니다.)

Base code의 변경점

Dynamic module을 추가함에 따라 Base moudle에도 해당 모듈에 대한 정보가 추가됩니다. [4]

app 단위의 build.gradle

android {
    ....
    dynamicFeatures = [':newfeature1', ':newfeature2']
}

Dynamic module에 대한 모듈 이름을 추가하여 base module에 dynamic module이 표시될 수 있도록 합니다.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.2.0'
    api 'androidx.core:core-ktx:1.3.2'
    api 'androidx.constraintlayout:constraintlayout:2.0.4'
    api 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
    api 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
    api "androidx.activity:activity-ktx:1.1.0"

    api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1"

   ...
   }

Dynamic Module과 공통으로 사용하는 library들을 implementation -> api로 변경해야 합니다. 그렇지 않으면 Dynamic Module에서 해당 library들에 대한 정의가 추가로 되어야 합니다. 여러 곳에서 library가 추가되어 버전 관리가 어려워지는 것보다는 api로 변경되어 빌드는 느려지겠지만 lib 버전 관리를 base 모듈 한 곳에서만 할 수 있는 점이 장점입니다. 하지만 양쪽에 정의할지, api를 사용할지는 선택 사항입니다.

strings.xml

<string name="title_newfeature1">New Feature</string>
<string name="title_newfeature2">New Feature2</string>

base에 모듈의 strings.xml에 모듈의 title이 추가됩니다. 이 이름은 모듈 다운로드 시 user에게 표시될 수 있는 이름입니다.

따라서 resource로 포함되어야 하나 다국어를 지원할 필요는 없습니다. (오히려 국가별 strings.xml에 추가하여 넣으면 오류가 발생합니다.)

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
}

배포 시 build.gradle에 리소스 최적화를 위해서 shrinkResources를 사용한다면 해당 리소스가 삭제될 수 있습니다. 따라서 tools:keep으로 예외 처리를 해야 합니다. [3]

Dynamic Feature Module의 기본 코드

build.gradle

plugins {
    id 'com.android.dynamic-feature'
    id 'kotlin-android'    
}
android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        minSdkVersion 23
        targetSdkVersion 30

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation project(":app")
   ...
}

- plugin 설정

plugin에 com.android.dynamic-feature를 적용합니다. [5]

- complieSdkVersion, BuildToolsVersion, minSdkVersion, targetSdkVersion

해당 값은 base 모듈의 값과 동일하게 사용하는 것이 좋습니다. 다른 곳에서(project 단위 build.gradle) 변수로 설정하고 base 모듈을 비롯한 각 모듈에서 가져다 쓰는 형태로 처리하는 게 좋습니다.

- versionCode, versionName

버전 코드는 Base module에만 정의합니다. Google Play가 기본 모듈에 있는 버전 코드를 이용하여 App bundle에서 생성한 모든 APK에 동일한 버전 코드를 할당합니다. 따라서 Base든 Dynamic module이든 추후 새로운 코드나 리소스를 업데이트하려면 base module의 버전을 변경하여 App bundle을 재 배포해야 합니다.

- minifyEnabled

proguard 적용을 위한 해당 속성은 base 모듈에 설정을 따릅니다. 따라서 Dynamic module에서는 해당 설정이 존재하면 안 됩니다. 하지만, proguard 규칙은 proguradFiles로 모듈별로 따로 정의할 수 있습니다. 다만 이 규칙은 빌드시 base 모듈을 비롯한 모든 모듈의 규칙과 병합됩니다. 따라서 해당 규칙은 프로젝트의 모든 모듈에 영향을 미칩니다.

- compileOptions

자바 버전 명시가 필요할 경우 Dynamic module에 따로 해야 합니다. Base module에 해당 명시가 있더라도 이는 별개로 판단되어 Dynamic module에서 필요하나, 명시가 없다면 컴파일 시 오류가 발생합니다.

- implementation project(":app")

dependencies에 base 모듈을 추가시킵니다. 따라서 Dynamimodule이 base app을 갖는 형태의 구조가 됩니다. 이는 상호 모듈 간 호출하는데 영향을 미치므로 이 부분은 다음 Dynamic Module의 코드를 다루는 포스팅에서 추가적으로 언급합니다.

manifest.xml

각각의 Module들은 Manifest.xml 파일을 갖습니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    package="com.example.dynamicfeature.ondemand.newfeature1">

    <dist:module
        dist:instant="false"
        dist:title="@string/title_newfeature2">
        <dist:delivery>
            <dist:on-demand />
        </dist:delivery>
        <dist:fusing dist:include="true" />
    </dist:module>
    
     <application>
        <activity android:name=".Feature1MainActivity"></activity>
    </application>
</manifest>

package 명이 표시됩니다.

<dist>... </dist> 안에는 Dynamic Module의 각종 명세가 들어갑니다.

  • dist:instant : instant 형태의 모듈 여부를 표기합니다.
  • dist:title:  모듈 이름을 표시합니다. 해당 resource는 base module의 strings.xml에 포함되어야 합니다.
  • dist:delivery: 모듈의 설치 형태를 정의합니다.
    • install-time: 앱 설치와 함께 설치됨
    • on-demand: 사용자의 요청에 의해 설치됨
  • dist:fusing dist:include: fusing 사용 여부를 결정합니다.

조건부 다운로드

install-time로 선택한 경우 조건부 전송 조건을 추가할 수 있습니다. 이때 설정할 수 있는 조건은 크게 세 가지입니다. [7]

  • 하드웨어 지원 여부 및 소프트웨어 버전(Open GL ES 버전)
  • 국가별
  • SDK 버전 수준(API 버전)

설정된 조건들은 manifest.xml의 dist:conditions 항목 안에 나열됩니다.

<dist:module ... >
  <dist:delivery>
      <dist:install-time>
          <dist:conditions>
              <!-- If you specify conditions, as described in the steps
                   below, the IDE includes them here. -->
          </dist:conditions>
      </dist:install-time>
  </dist:delivery>
</dist:module>

하드웨어 및 소프트웨어 버전 조건 생성

install-time inclusion 항목에서 "Only include module at install-time for devices with specified featrues"를 선택하면 설치 조건을 선택할 수 있습니다.

먼저 첫 번째 select box를 "OpenGL ES Version"을 선택하기 위한 버전을 명시할 수 있습니다.

첫 번째 항목에 Name을 넣는 경우 PackageManager에서 제공하는 FEATURE_XXX 상수 목록을 기준으로 조건을 넣을 수 있습니다. [6]

CARMERA_AR 지원 여부, Bluetooth LE 지원 여부, NFC 지원 여부 등등 여러 가지 옵션들을 설정할 수 있습니다.

두 번째 select box를 선택할 때 원하는 항목을 넣으면 위 그림과 같이 자동 완성되어 표기해 줍니다.

국가별 설치

아래와 같이 표기하면 해당 국가를 제외하고 나머지 국가에서는 install-time에 해당 모듈이 설치됩니다.

<dist:conditions>
  <dist:user-countries dist:exclude="true">
    <dist:country dist:code="CN"/>
    <dist:country dist:code="HK"/>
  </dist:user-countries>
</dist:conditions>

dist:exclude=true로 설정했으므로 설정된 나라에서는 모듈이 설치되지 않도록 되어 있습니다. 위 예제에서는 CN, HK를 설정했으므로 중국과 홍콩에서는 설치되지 않습니다. 국가는 CLDR country code로 표기합니다. [8]

API(SDK) 버전별 설치

<dist:conditions>
   <dist:min-sdk dist:value="21"/>
   <dist:max-sdk dist:value="24"/>
</dist:conditions>

위 예제는 SDK 버전의 상한 과 하한을 전부 명시했습니다. 따라서 21, 22, 23, 24 버전에서만 해당 모듈이 설정됩니다.

 

Summary

Dynamic module을 생성하는 방법은 어렵지 않습니다. Android Studio에서 간단한 절차를 통해서 손쉽게 모듈을 추가할 수 있습니다. 다만 생성 시 정해진 규칙이나 주의점은 숙지하고 생성해야 추후 에러 발생의 소지를 조금이나마 줄일 수 있다는 생각이 듭니다.

정리하면서 또는 이 글을 읽으면서 "아.. 이런 기능은 분리했었으면 좋을 뻔했다"라고 생각이 들 수도 있습니다. 이번 글의 내용은 대부분 Android Developer에서 산발적으로 분산되어 있던 내용을 들 정리해서 담았습니다. 다음 포스팅에서는 실제로 Dynamic Module에서 사용하는 코드들 (SplitCompat 등)에 대한 내용을 다룹니다. 또한 어떻게 테스트해야 하는지와 Dynamic Moudle을 실제 적용할 때 장단점에 대해서 얘기해 보겠습니다.

References

[1] developer.android.com/guide/app-bundle/play-feature-delivery

[2] developer.android.com/guide/app-bundle/asset-delivery

[3] developer.android.com/studio/build/shrink-code

[4] developer.android.com/guide/app-bundle/configure-base

[5] developer.android.com/guide/app-bundle/on-demand-delivery

[6] developer.android.com/reference/android/content/pm/PackageManager#constants

[7] developer.android.com/guide/app-bundle/conditional-delivery

[8] en.wikipedia.org/wiki/ISO_3166-1_alpha-2#TG

 

반응형