[Android][Kotlin] ProductFlavors와 BuildType

D.O·2023년 10월 3일
0

들어가며

어느 정도 완성된 mineme project를 기준으로 전체적인 리팩토링을 진행하려한다.

이미 백엔드 담당 하던 선배들은 취업을 하고 손을 놔버렸지만…(서비스 하기로 해놓고 ㅡ.ㅡ) 나는 어느정도 애정이 있던 프로젝트여서 최적화 및 리팩토링을 하면서 계속해서 공부하려고 한다.

그 전에 리팩토링 전후로 성능 측정을 하고 결과를 보여주고싶어서 성능 측정 방법과 앱최적화 방법에 대해서 공부하던 도중 대부분의 최신 컴파일러는 난독화 및 최적화 기법(reorder와 inlining)이 적용이 되기때문에 앱을 프로파일링할 때 실제 소스 코드와 프로파일링 결과 사이에 차이가 발생할 수 있다고 한다.

따라서, 정확한 프로파일링 결과를 얻기 위해서 난독화나 최적화가 적용되지 않는 디버그 빌드를 사용하는 것이 좋다고 한다.

이 것을 보는 순간 이전에 오픈소스를 분석하며 간단하게 공부하고 넘어갔던 BuildTypeProductFalavor가 떠올랐다.

"ProductFlavors와 BuildType을 사용하면 여러 버전의 앱을 생성하면 다양한 상황과 요구 사항에 따라 앱을 더 유연하게 관리할 수 있다.”

이 정도만 알고 넘어갔는데 앱 최적화 및 리팩토링을 진행하기 전에 정확한 성능 측정을 위해 ProductFlavors와 BuildType에 대해 자세히 공부해고 실제 내 프로젝트에 적용해보겠다.


먼저 개념부터 알아볼게요
상황을 예로 생각해보면 이해하기 더 쉬울 겁니다

상황

사진 편집 및 공유를 위한 모바일 앱을 무료 버전과 프리미엄 버전이 있으며, 개발자는 앱의 성능과 기능을 테스트하기 위한 다양한 버전을 생성하려고 한다.

사실 요즘에는 무료 버전과 유료 버전을 별도의 앱으로 분리하는 경우가 거의 없지만 간단한 예를 위해서..

1. ProductFlavors

  • free: 무료 버전의 앱. 기본적인 사진 편집 기능만 제공하며, 광고가 포함
  • premium: 유료 버전의 앱. 고급 편집 기능, 광고 없음, 클라우드 저장 기능 등 추가 기능을 제공

2. BuildType

  • debug: 개발 및 테스트를 위한 버전. 로그 및 디버깅 정보가 포함되어 있어, 개발자가 문제를 쉽게 파악하고 수정할 수 있습니다.
  • release: 최종 사용자를 대상으로 한 버전. 최적화되어 있으며, 디버깅 정보나 로그가 제거되어 앱의 성능과 보안이 향상됩니다.

조합 및 사용 사례

  1. freeDebug
    • 의미: 무료 버전의 개발 및 테스트 앱.
    • 사용 사례: 개발자가 새로운 기능을 추가하거나 버그를 수정한 후, 무료 버전의 앱에서 해당 기능이나 수정 사항이 제대로 작동하는지 확인하기 위해 사용합니다.
  2. freeRelease
    • 의미: 무료 버전의 최종 배포 앱.
    • 사용 사례: 앱 스토어에 배포하기 전의 무료 버전. 광고와 기본 편집 기능이 포함되어 있습니다.
  3. premiumDebug
    • 의미: 프리미엄 버전의 개발 및 테스트 앱.
    • 사용 사례: 개발자가 프리미엄 버전의 새로운 고급 기능을 추가하거나 버그를 수정한 후, 해당 기능이나 수정 사항이 제대로 작동하는지 확인하기 위해 사용합니다.
  4. premiumRelease
    • 의미: 프리미엄 버전의 최종 배포 앱.
    • 사용 사례: 앱 스토어에 배포하기 전의 프리미엄 버전. 고급 편집 기능, 광고 없음, 클라우드 저장 기능 등이 포함되어 있습니다.

“여러 버전의 앱을 생성하면 다양한 상황과 요구 사항에 따라 앱을 더 유연하게 관리할 수 있다” 는 말이 이해가 조금 되시나요?!

정리하자면

ProductFlavors는 앱의 다양한 변형을 정의하는 데 사용됩니다. 이를 통해 개발자는 하나의 코드베이스에서 여러 버전의 앱을 생성할 수 있습니다.

BuildType는 앱의 빌드 및 배포 설정을 정의하는 데 사용됩니다.

보통 BuildType은 DebugRelease, Benchmark 정도 두고

ProductFlavors는 목적에 맞게 다양하게 변형이 되는 방식이 일반적이라고 합니다.

ProductFlavors 다른 예시들로는

  • international: 국제 시장을 대상으로 한 버전. 다양한 언어와 지역 설정을 지원합니다.

  • korea: 한국 시장을 대상으로 한 버전. 한국어와 특정 지역 콘텐츠를 제공합니다.

  • consumer: 일반 소비자를 대상으로 한 버전.

  • business: 기업 및 사업자를 대상으로 한 버전. 추가적인 기업 관리 기능을 제공합니다.

  • lite: 무료 버전. 기본적인 게임 플레이를 제공하며, 광고가 포함되어 있습니다.

  • pro: 유료 버전. 추가 콘텐츠와 광고 없는 경험을 제공합니다.

이제 확실히 느낌이 오시죠?

저는 “Now In Android”라는 오픈소스를 분석하며 공부했기 때문에 “Now In Android”는 어떻게 이 것들을 적용했는지 찾아보고 참고해서 적용해봤습니다

친절하게도 Readme에 다 설명이 되어있습니다.

요약하자면 이 앱은 debug, release, 그리고 성능 측정을 위한 benchmark 세 가지의 buildType을 포함하고 있다고 해요

콘텐츠 로드 위치를 제어하기 위해 demoprod 두 가지의 productFlavor를 사용하며 (demo는 샘플 데이터를 사용하고, prod는 실제 서버 데이터를 사용)

일반적인 개발에는 demoDebug를, UI 성능 테스트에는 demoRelease를 사용하는 것이 권장한다고 합니다.

즉 이것의 조합을 설명하자면

proddebug:

  • 의미: 실제 서버와 연결된 앱의 개발 및 테스트 버전.

prodrelease:

  • 의미: 실제 서버와 연결된 앱의 최종 배포 버전.

demodebug:

  • 의미: 데모 데이터와 함께 제공되는 앱의 개발 및 테스트 버전.

demorelease:

  • 의미: 데모 데이터와 함께 제공되는 앱의 최종 배포 버전.

이렇게 정리할 수 있습니다.

넘어가서

이제 이러한 build 변형을 어떻게 안드로이드 프로젝트에 적용하는지 한번 알아보겠습니다.

1. BuildType 설정:

기본적으로 안드로이드 프로젝트는 debugrelease 두 가지 BuildType을 제공합니다.

android {
    ...

    buildTypes {
        debug {
            applicationIdSuffix ".debug"
            versionNameSuffix "-debug"
            debuggable true
        }

        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            debuggable false
        }
    }
}
  • debug: 개발 및 테스트를 위한 버전. applicationIdSuffixversionNameSuffix를 사용하여 패키지 이름과 버전 이름에 접미사를 추가할 수 있습니다.
  • release: 최종 사용자를 대상으로 한 버전. minifyEnabled는 코드 난독화를 활성화합니다.

2. ProductFlavors 설정

android {
    ...

    flavorDimensions "versionType"

    productFlavors {
        free {
            dimension "versionType"
            applicationId "com.example.myapp.free"
            versionName "1.0-free"
        }

        paid {
            dimension "versionType"
            applicationId "com.example.myapp.paid"
            versionName "1.0-paid"
        }
    }
}
  • flavorDimensions: 플레이버 차원을 정의합니다. 여기서는 "versionType"이라는 차원을 사용합니다.
  • free: 무료 버전의 앱 설정입니다. applicationIdversionName을 통해 패키지 이름과 버전 이름을 지정합니다.
  • premium: 유료 버전의 앱 설정입니다.

이런식으로 적용하고 싶은 모듈의 build.gradle 파일에 적용하시고 build variants를 클릭하면 적용이 되어 있습니다.

아래는 NowInAndroid의 build variants입니다.

ProductFlavors를 사용하여 앱의 여러 버전을 만들 때, 각 플레이버에 따라 코드를 변경하려면 주로 다음 두 가지 방법을 사용합니다

1. 소스 세트(Source Sets)

ProductFlavor에 대해 별도의 소스 디렉토리를 생성하여 플레이버별로 다른 코드나 리소스를 제공할 수 있습니다.

예를 들어

src/
|-- main/               # 모든 플레이버에서 공통으로 사용되는 코드와 리소스
|   |-- java/
|   |-- res/
|-- free/               # free 플레이버 전용 코드와 리소스
|   |-- java/
|   |-- res/
|-- paid/               # paid 플레이버 전용 코드와 리소스
|-- java/
|-- res/

이렇게 하면, free 플레이버로 빌드할 때 free 디렉토리의 코드와 리소스가 사용되고, paid 플레이버로 빌드할 때 paid 디렉토리의 코드와 리소스가 사용됩니다.

2. 코드 내 조건문

BuildConfig 클래스를 사용하여 코드 내에서 현재 빌드된 플레이버를 확인하고 조건문을 사용하여 플레이버별로 다른 동작을 수행할 수 있습니다.

if (BuildConfig.FLAVOR.equals("free")) {
    // free 플레이버에 대한 코드
} else if (BuildConfig.FLAVOR.equals("paid")) {
    // paid 플레이버에 대한 코드
}

이 방법은 간단한 조건부 로직에 적합하며, 큰 코드 변경이 필요하지 않은 경우에 유용합니다.

여러 모듈에서 동일한 buildType이나 productFlavor를 사용할 경우, 호환성 문제를 방지하기 위해 모든 모듈에 동일한 설정을 적용하는 것이 필수적입니다.

제가 분석한 오픈소스 프로젝트에서는 플러그인으로 Flavor를 관리하는데 제가 생각한 이유는 이러한 호환성 문제를 방지하고, 빌드 로직의 재사용성과 중앙 관리의 편의성을 추구하기 위해 특정 productFlavor 설정을 사용자 정의 플러그인으로 구현한 것이라 생각이 됩니다. 이 사용자 정의 플러그인은 build-logic 모듈에 정의되어 있어, 프로젝트의 여러 모듈에서 통일된 방식으로 쉽게 적용할 수 있습니다.

build-logic에 대해서는 후에 다른 글에서 따로 다루겠습니다.

저는 오픈소스를 참고하여 Flavor를 build-logic 모듈에서 플러그인으로 관리했습니다

아래는 코드 예시입니다.

FlavorDimension은 여러 ProductFlavor들을 그룹화하는 역할을 합니다.

여기서는 contentType이라는 하나의 차원만 정의했습니다

다른 차원을 추가하고 싶으면 이곳에 추가하면 됩니다.

DoFlavor는 두 가지 플레이버, 즉 demoprod를 정의합니다. demo는 데모 버전을 위한 것이며, prod는 실제 프로덕션 버전을 위한 것입니다. 각 플레이버는 FlavorDimensioncontentType에 속합니다. 또한, demo 플레이버는 앱의 applicationId.demo 접미사를 추가했습니다.

configureFlavors 함수는 CommonExtension을 확장하여 플레이버를 구성합니다

configureFlavors 함수를 사용하면 이러한 플레이버 설정을 쉽게 적용할 수 있습니다.

아래 코드는 이 함수를 사용하여 AndroidApplicationFlavorsConventionPlugin이라는 사용자 정의 Gradle 플러그인을 정의하고 있습니다. 이 플러그인은 ApplicationExtension (안드로이드 앱 모듈에 대한 Gradle 확장)을 구성하여 configureFlavors 함수를 사용해 플레이버를 설정합니다.

이제 Flavor를 설정했으므로, 해당 Flavor를 기반으로 한 코드 분리 방법에 대해 간단히 설명하겠습니다.

저는 Flavor를 구분할 때 proddemo로 구분을 했었는데요

콘텐츠 로드 위치를 구분하기 위해 이렇게 하였고 demo는 샘플 데이터를 사용하고, prod는 실제 서버 데이터를 사용하도록 하기위해 이렇게 구분하였습니다.

저는 별도의 소스 디렉토리를 생성하여 플레이버별로 다른 코드나 리소스를 제공하는 방식으로 진행하였습니다.

Build Variant 탭에서 플레이버를 Prod로 변경하면 아래와 같이 Active되는 플레이버에 맞는 폴더가 자동으로 표시됩니다.

Prod 환경에서는 Retrofit을 사용하여 실제 서버 데이터에 접근하는 클래스를 구현했고

Demo 환경에서는 실제 서버 대신 샘플 데이터를 사용하도록 설계하였습니다.

다음으로 Build Type을 어떤 방식으로 적용했는지 설명드리겠습니다.

Build Type

저는 app 모듈의 build.gradle에 아래 처럼 정의해 두었습니다.

다른 모듈에는 명시하지 않았는데 그 이유는 app모듈에 다른 모듈들이 전부 의존적이기 때문에 app 모듈이 의존하는 모든 모듈들도 동일한 buildType으로 컴파일됩니다.

하위 라이브러리 모듈에는 기본적인 debugrelease 두 가지 buildType 만으로 충분하다 생각되어 따로 추가하지 않았습니다.

  1. debug:
    • applicationIdSuffix: 앱의 패키지 이름에 .DEBUG 접미사를 추가합니다. 이렇게 하면 동일한 기기에 debug 및 release 버전의 앱을 동시에 설치할 수 있습니다.
  2. release:
    • isMinifyEnabled: 코드 축소를 활성화하여 APK 크기를 줄입니다.
    • applicationIdSuffix: 앱의 패키지 이름에 .RELEASE 접미사를 추가합니다.
    • proguardFiles: ProGuard 설정 파일을 지정하여 코드 난독화 및 최적화를 수행합니다.
    • signingConfig: 앱 서명 설정을 지정합니다. 여기서는 debug 서명 키를 사용하도록 설정되어 있습니다. 실제 배포를 위해서는 별도의 서명 키가 필요합니다.
  3. benchmark:
    • initWith(release): release 빌드 타입의 설정을 기반으로 benchmark 빌드 타입을 초기화합니다.
    • matchingFallbacks: 이 빌드 타입에 대한 의존성 해결에 실패할 경우 대체로 사용할 빌드 타입을 지정합니다.
    • signingConfig: 앱 서명 설정을 지정합니다. 여기서는 debug 서명 키를 사용하도록 설정되어 있습니다.
    • proguardFiles: benchmark 전용 ProGuard 설정 파일을 지정합니다.
    • isMinifyEnabled: 코드 축소를 활성화합니다.
    • applicationIdSuffix: 앱의 패키지 이름에 .BENCHMARK 접미사를 추가합니다.

여기서 matchingFallbacks 속성은 뭘까요?

앱 모듈에서 빌드 타입을 선택하면, 라이브러리 모듈의 동일한 빌드 타입이 자동으로 선택됩니다.

하지만 benchmark는 app 모듈에만 정의를 해두었기 때문에 benchmark로 변경할 시 종속 모듈에 없는 경우가 발생합니다.

이러한 경우 원래 빌드는 실패하게 됩니다.

이 빌드 실패를 방지하기 위해 matchingFallbacks를 사용하여 대체할 buildType을 지정할 수 있습니다.

이렇게 buildType과 Flavor을 정의하면 Build Variant 탭에 자동으로 이렇게 적용이 됩니다.!

마치며

ProductFlavors와 BuildType에 대해 조금 더 명확하게 이해하셨나요?

이 주제를 깊게 다루게 된 계기는 앱의 성능 측정을 위해 build 변형을 고려하면서였습니다. 실제 프로젝트에서 이 두 개념은 다양한 목적으로 필수적으로 활용될 수 있습니다.

ProductFlavors와 BuildType의 개념과 활용법을 자세히 알아보았습니다. 이를 통해 프로젝트에서 필요한 부분에 맞게 flavor와 buildType을 구분하여 여러분의 목적에 맞게 효율적으로 개발할 수 있을 것입니다.

개인적으로 이 개념들은 복잡하다기보다는 흥미로웠습니다. 공부하는 동안 재미를 느꼈습니다.

마지막까지 읽어주셔서 감사합니다.

profile
Android Developer

0개의 댓글