/ ANDROID, GRADLE, PRODUCTFLAVORS, BUILDTYPES

ProductFlavors와 BuildType 조합

Gradle에서 ProductFlavors와 BuildType을 이용해서 다양한 형상을 만들 수 있습니다. Gradle을 기반으로 BuildTypes은 기본으로하고 ProductFlavors에 여러가지 dimension을 정의하여 조합하여 빌드를 간편하게 생산 할 수있습니다. 간략한 예를 들어 상황을 정의하고 실제 build.gradle 파일을 수정해 보겠습니다.

서비스가 점점 커지다 보면 일명 “OO향”빌드를 배포해야하는 경우나 “XX용”빌드를 따로 뽑아야하는 경우가 생깁니다. 가령 예를 들자면 “중국향”과 “국내향”처럼 나눌수 있을 것이고, “개발용”,”QA용”,”내부배포용”,”실서비스용”으로도 나눌 수도 있습니다. 이런 구성을 개발에서 각 버전을 구분하여 관리해야 한다면 우리는 어떻게 해야 할까요?

groovy 언어가 익숙치 않고 gradle에 대한 학습할 기회가 적다보니 build.gradle파일을 편집하기는 꽤 부담스럽습니다. 조금만 바뀌어도 빌드가 안되거나 많은 사이드 이펙트를 만들 수 있기 때문인데요. 오늘은 아래의 build.gradle 파일을 처음부터 작성해보면서 각 구문의 의미와 용법을 설명해 보겠습니다. 알고나면 정말 단순합니다.

plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.2"

    defaultConfig {
        applicationId "com.al.mond.oreo"
        minSdkVersion 23
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }


    flavorDimensions  "store","server"
    productFlavors {
        googlePlay {
            dimension "store"
            applicationIdSuffix ".google"
            resValue "string", "storeInfo", "OOOOOO"
        }
        oneStore {
            dimension "store"
            applicationIdSuffix ".onestore"
            resValue "string", "storeInfo", "XXXXXX"
        }
        dev {
            dimension "server"
            resValue "string", "server_url", "https://test.oreo.com/rest"
        }
        prod {
            dimension "server"
            resValue "string", "server_url", "https://oreo.com/rest"
        }
    }

    buildTypes {
        release {
            //스토어 배포
            minifyEnabled true
            debuggable false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }

        inhouse {
            //내부 배포
            initWith release
            debuggable true
        }

        debug {
            //개발
            minifyEnabled false
            debuggable true
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

아주 간단한 시나리오.

다양한 환경에 배포를 목표로 하는 오레오라는 앱이 있습니다. 개발 단계는 개발,내부배포,상용배포로 진행됩니다. 앱은 GooglePlay,OneStore에 등록합니다. 각 마켓별로 다른 프로모션을 적용 할 예정이기 때문에 빌드에 따라 앱이 다르게 동작해야합니다. 그리고 GooglePlay에서는 초코오레오 라는 이름으로 등록되고, OneStore에서는 치즈오레오로 등록됩니다. 두 앱은 동시에 설치가 가능합니다.

Android Studio에서 기본 프로젝트중 가장 단순한 Empty Activity를 기반으로 프로젝트를 생성합니다. app/build.gradle을 열어보면 아주 심플한 빌드환경 설정을 볼 수 있습니다. 오늘은 이 프로젝트로 예제를 구성해 보겠습니다.

new

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.2"

    defaultConfig {
        applicationId "com.al.mond.oreo"
        minSdkVersion 23
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

BuildTypes

    ...
    buildTypes {
        release {
            //스토어 배포
            signingConfig signingConfigs.release
            minifyEnabled true
            debuggable false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }

        inhouse {
            //내부 배포
            initWith release
            applicationIdSuffix ".inhouse"
            debuggable true
        }

        debug {
            //개발
            applicationIdSuffix ".debug"
            signingConfig signingConfigs.debug
            minifyEnabled false
            debuggable true
        }
    }
    ...

우선 빌드의 타입을 가장 보편적인 형태를 따라 상용빌드(release), 내부빌드(inhouse), 디버그빌드(debug)로 나누어 두었습니다. inhouse빌드는 대부분의 속성을 initWith release를 통해서 release빌드의 속성을 그대로 상속받고 필요한 속성인 debuggableture로 변경했습니다.

applicationIdSuffix옵션을 통해 각 빌드의 applicationId를 다르게 구성 했습니다. 이렇게 applicationId이 다르면 applicationId가 중복되지 않기 때문에 하나의 폰에 여러가지 빌드타입을 설치하고 기능을 비교할 수 도 있습니다. 물론 앱 아이콘과 앱 이름을 구분하여 서로 헷갈리지 않도록 함께 처리해두어야 합니다. inhouse빌드는 applicationId가 com.al.mond.oreo.inhouse가 되고 release빌드는 suffix가 없으니 applicationId가 com.al.mond.oreo이 됩니다.

그리고 필수는 아니지만 inhouse빌드는 release와 동일하게 난독화된 상태로 진행하는 것을 추천하고 싶습니다. inhouse에서 디버깅의 편리함을 위해 minifyEnabledfalse로 둔다면, 난독화를 통해 클래스나 필드이 변경되기 때문에 이름에 의존적인 코드가 실수로 난독화 되어버린다면, 검수단계에서 발견하지 못하게 됩니다. 예를들어 API호출도 잘 되었고 서버에서 값도 잘 내려주었지만, DAO클래스가 난독화 되어 데이터를 파싱하지 못 할 수 있습니다. 이렇게 되면 검수는 난독화하지 않은 상태로 했기 때문에 잘 동작하고, 실배포에서는 문제가 발생합니다. 때문에 inhouse빌드는 디버그할때 번거롭더라도 항상 minifyEnabledtrue로 두어 난독화를 시켜줍시다.

정의된 빌드에 따라 실행은 Android Studio 화면 왼쪽 모서리쪽에 접혀있는 build variants 사이드바를 클릭하여 열어주세요.
Product Flavors build variant

이제 아래의 그림처럼 release,inhouse,debug 빌드를 선택할 수 있게 되었습니다. Product Flavors build variant debug

BuildType은 보통 목적이 뚜렷하고 이미 속성들이 잘 갖추어 져 있기때문에 생각 할 부분이 적습니다.

ProductFlavors

이제 ProductFlavors를 정의해 보겠습니다.

flavorDimensions "store"

ProductFlavors를 정의하기에 앞서 어떤 차원에서 사용 할 것인지 이름을 정의해야 합니다. Dimensions의 의미가 처음에는 와 닿지 않습니다. 우리가 정의하는 항목의 수 많큼 배열의 차원으로 생각하면 이해하기가 한결 쉬워집니다. 적절한 예가 바로 떠오르지 않아서 조금 억지스럽지만 “스토어”별로 googlePlay와 OneStore 나누고, “과금스타일”별로 유료와 무료 빌드를 나눈다면 2x2의 2차원 배열이 됩니다. 여기에 “서버”까지 내부망과 외부망용으로 구분하게되면 2x2x2의 3차원 배열이 됩니다. Dimensions의 의미가 잘 이해 되셨나요?

    flavorDimensions "store"
    productFlavors {
        googlePlay {
            applicationIdSuffix ".google"
            dimension "store"
            resValue "string", "storeInfo", "OOOOOO"
        }
        oneStore {
            applicationIdSuffix ".onestore"
            dimension "store"
            resValue "string", "storeInfo", "XXXXXX"
        }
    }

buildType에서와 마찬가지로 applicationIdSuffix를 이용하여 각각 다른 applicationId를 할당합니다. 구글플레이에 등록예정인 인하우스 빌드의 applicationId는 com.al.mond.oreo.google.inhouse가 됩니다. 각 스토어에 따라 다른 String값을 가져야 한다면 res/value/string.xml대신 resValue를 이용해서 정의 할 수 있습니다.

이렇게 아주 간단한 1차원 배열의 store Dimension을 정의하고 googlePlay과 oneStore 두가지 선택지를 추가 했습니다. 이제 서버 dimension을 추가해서 2차원 배열의 Dimension을 아래처럼 구성 할 수 있습니다. 빌드에 따라 서버 API의 BaseUrl도 달라지도록 준비 해봤습니다.

    flavorDimensions "store", "server"
    productFlavors {
        googlePlay {
            dimension "store"
            applicationIdSuffix ".google"
            resValue "string", "storeInfo", "OOOOOO"
        }
        oneStore {
            dimension "store"
            applicationIdSuffix ".onestore"
            resValue "string", "storeInfo", "XXXXXX"
        }
        dev {
            dimension "server"
            resValue "string", "server_url", "https://test.oreo.com/rest"
        }
        prod {
            dimension "server"
            resValue "string", "server_url", "https://oreo.com/rest"
        }
    }

productFlavors의 dimension을 추가하고 관리할 때 주의점들

buildType의 이름과 productFlavors의 이름은 서로 동일한 이름을 사용 할 수 없습니다. 추후에 서버의 타입이 “debug”인 서버가 추가되면 다른 이름을 사용하거나 dimension의 정의에 대해 고민을 해야할 시점입니다.

경험상 대부분의 프로젝트들은 그렇게 많은 dimension이 필요하지는 않습니다. 아시다시피 배열의 차원이 늘어나면, 조합 가능한 경우의 수가 기하급수적으로 늘어납니다. 결국 사용하지 않는 BuildVariant가 많아 지고, 관리 되지않는 무의미한 dimension이 되지 않도록 주의해야 합니다.
2개의 스토어 x 2개의 서버 x 3개의 빌드타입 = 12가지 변형 벌써 눈이 핑핑 돌기 시작하네요.

두개이상의 Dimension이 정의되면 순서에 유의해야합니다. "store","server" 일때와 "server","store"일때는 생성되는 BuildVariant도 달라지게 됩니다.

Product Flavors build variant Product Flavors build variant

순서가 변경되면 각 설정의 조합이나 변형에 따라 다르게 읽어들이게 될 Res폴더의 경로도 변경 되어야 하고, 혹시 배포 자동화를 위해 만들어둔 스크립트가 있다면 변경되어야 합니다. 어렵진 않지만 시력을 잃을 수 도 있습니다.

빌드변형들(build variants)의 이름은 카멜표기법을 따르며 flavorDimensions에 정의된 순서대로 나열하고 마지막에 buildType을 쓰면 됩니다. 로컬에서 빌드 할 때는 안드로이드 스튜디오가 알아서 메뉴를 만들어 주기 때문에 우리는 잘 선택하기만 하면 됩니다.

도입부에 올려둔 build.gradle파일을 다시 보면 처음 이 글을 읽기 시작했을 때와 다른 느낌이 드시나요? 다음 포스트에서는 각 빌드변형별로 다른 아이콘, 다른 앱이름을 붙여보도록 하겠습니다. (LINK)

plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.2"

    defaultConfig {
        applicationId "com.al.mond.oreo"
        minSdkVersion 23
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }


    flavorDimensions  "store","server"
    productFlavors {
        googlePlay {
            dimension "store"
            applicationIdSuffix ".google"
            resValue "string", "storeInfo", "OOOOOO"
        }
        oneStore {
            dimension "store"
            applicationIdSuffix ".onestore"
            resValue "string", "storeInfo", "XXXXXX"
        }
        dev {
            dimension "server"
            resValue "string", "server_url", "https://test.oreo.com/rest"
        }
        prod {
            dimension "server"
            resValue "string", "server_url", "https://oreo.com/rest"
        }
    }

    buildTypes {
        release {
            //스토어 배포
            minifyEnabled true
            debuggable false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }

        inhouse {
            //내부 배포
            initWith release
            debuggable true
        }

        debug {
            //개발
            minifyEnabled false
            debuggable true
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

Search

Get more post