어느 정도 완성된 mineme project를 기준으로 전체적인 리팩토링을 진행하려한다.
이미 백엔드 담당 하던 선배들은 취업을 하고 손을 놔버렸지만…(서비스 하기로 해놓고 ㅡ.ㅡ) 나는 어느정도 애정이 있던 프로젝트여서 최적화 및 리팩토링을 하면서 계속해서 공부하려고 한다.
그 전에 리팩토링 전후로 성능 측정을 하고 결과를 보여주고싶어서 성능 측정 방법과 앱최적화 방법에 대해서 공부하던 도중 대부분의 최신 컴파일러는 난독화 및 최적화 기법(reorder와 inlining)이 적용이 되기때문에 앱을 프로파일링할 때 실제 소스 코드와 프로파일링 결과 사이에 차이가 발생할 수 있다고 한다.
따라서, 정확한 프로파일링 결과를 얻기 위해서 난독화나 최적화가 적용되지 않는 디버그 빌드를 사용하는 것이 좋다고 한다.
이 것을 보는 순간 이전에 오픈소스를 분석하며 간단하게 공부하고 넘어갔던 BuildType
와 ProductFalavor
가 떠올랐다.
"ProductFlavors와 BuildType을 사용하면 여러 버전의 앱을 생성하면 다양한 상황과 요구 사항에 따라 앱을 더 유연하게 관리할 수 있다.”
이 정도만 알고 넘어갔는데 앱 최적화 및 리팩토링을 진행하기 전에 정확한 성능 측정을 위해 ProductFlavors와 BuildType에 대해 자세히 공부해고 실제 내 프로젝트에 적용해보겠다.
먼저 개념부터 알아볼게요
상황을 예로 생각해보면 이해하기 더 쉬울 겁니다
사진 편집 및 공유를 위한 모바일 앱을 무료 버전과 프리미엄 버전이 있으며, 개발자는 앱의 성능과 기능을 테스트하기 위한 다양한 버전을 생성하려고 한다.
사실 요즘에는 무료 버전과 유료 버전을 별도의 앱으로 분리하는 경우가 거의 없지만 간단한 예를 위해서..
free
: 무료 버전의 앱. 기본적인 사진 편집 기능만 제공하며, 광고가 포함premium
: 유료 버전의 앱. 고급 편집 기능, 광고 없음, 클라우드 저장 기능 등 추가 기능을 제공debug
: 개발 및 테스트를 위한 버전. 로그 및 디버깅 정보가 포함되어 있어, 개발자가 문제를 쉽게 파악하고 수정할 수 있습니다.release
: 최종 사용자를 대상으로 한 버전. 최적화되어 있으며, 디버깅 정보나 로그가 제거되어 앱의 성능과 보안이 향상됩니다.freeDebug
freeRelease
premiumDebug
premiumRelease
“여러 버전의 앱을 생성하면 다양한 상황과 요구 사항에 따라 앱을 더 유연하게 관리할 수 있다” 는 말이 이해가 조금 되시나요?!
정리하자면
ProductFlavors
는 앱의 다양한 변형을 정의하는 데 사용됩니다. 이를 통해 개발자는 하나의 코드베이스에서 여러 버전의 앱을 생성할 수 있습니다.
BuildType
는 앱의 빌드 및 배포 설정을 정의하는 데 사용됩니다.
보통 BuildType은 Debug와 Release, Benchmark 정도 두고
ProductFlavors는 목적에 맞게 다양하게 변형이 되는 방식이 일반적이라고 합니다.
ProductFlavors 다른 예시들로는
international
: 국제 시장을 대상으로 한 버전. 다양한 언어와 지역 설정을 지원합니다.
korea
: 한국 시장을 대상으로 한 버전. 한국어와 특정 지역 콘텐츠를 제공합니다.
consumer
: 일반 소비자를 대상으로 한 버전.
business
: 기업 및 사업자를 대상으로 한 버전. 추가적인 기업 관리 기능을 제공합니다.
lite
: 무료 버전. 기본적인 게임 플레이를 제공하며, 광고가 포함되어 있습니다.
pro
: 유료 버전. 추가 콘텐츠와 광고 없는 경험을 제공합니다.
이제 확실히 느낌이 오시죠?
저는 “Now In Android”라는 오픈소스를 분석하며 공부했기 때문에 “Now In Android”는 어떻게 이 것들을 적용했는지 찾아보고 참고해서 적용해봤습니다
친절하게도 Readme에 다 설명이 되어있습니다.
요약하자면 이 앱은 debug
, release
, 그리고 성능 측정을 위한 benchmark
세 가지의 buildType을 포함하고 있다고 해요
콘텐츠 로드 위치를 제어하기 위해 demo
와 prod
두 가지의 productFlavor를 사용하며 (demo
는 샘플 데이터를 사용하고, prod
는 실제 서버 데이터를 사용)
일반적인 개발에는 demoDebug
를, UI 성능 테스트에는 demoRelease
를 사용하는 것이 권장한다고 합니다.
즉 이것의 조합을 설명하자면
proddebug
:
prodrelease
:
demodebug
:
demorelease
:
이렇게 정리할 수 있습니다.
넘어가서
이제 이러한 build 변형을 어떻게 안드로이드 프로젝트에 적용하는지 한번 알아보겠습니다.
BuildType
설정:기본적으로 안드로이드 프로젝트는 debug
와 release
두 가지 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
: 개발 및 테스트를 위한 버전. applicationIdSuffix
와 versionNameSuffix
를 사용하여 패키지 이름과 버전 이름에 접미사를 추가할 수 있습니다.release
: 최종 사용자를 대상으로 한 버전. minifyEnabled
는 코드 난독화를 활성화합니다.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
: 무료 버전의 앱 설정입니다. applicationId
와 versionName
을 통해 패키지 이름과 버전 이름을 지정합니다.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
는 두 가지 플레이버, 즉 demo
와 prod
를 정의합니다. demo
는 데모 버전을 위한 것이며, prod
는 실제 프로덕션 버전을 위한 것입니다. 각 플레이버는 FlavorDimension
의 contentType
에 속합니다. 또한, demo
플레이버는 앱의 applicationId
에 .demo
접미사를 추가했습니다.
configureFlavors 함수는 CommonExtension
을 확장하여 플레이버를 구성합니다
configureFlavors
함수를 사용하면 이러한 플레이버 설정을 쉽게 적용할 수 있습니다.
아래 코드는 이 함수를 사용하여 AndroidApplicationFlavorsConventionPlugin
이라는 사용자 정의 Gradle 플러그인을 정의하고 있습니다. 이 플러그인은 ApplicationExtension
(안드로이드 앱 모듈에 대한 Gradle 확장)을 구성하여 configureFlavors
함수를 사용해 플레이버를 설정합니다.
이제 Flavor를 설정했으므로, 해당 Flavor를 기반으로 한 코드 분리 방법에 대해 간단히 설명하겠습니다.
저는 Flavor를 구분할 때 prod
와 demo
로 구분을 했었는데요
콘텐츠 로드 위치를 구분하기 위해 이렇게 하였고 demo
는 샘플 데이터를 사용하고, prod
는 실제 서버 데이터를 사용하도록 하기위해 이렇게 구분하였습니다.
저는 별도의 소스 디렉토리를 생성하여 플레이버별로 다른 코드나 리소스를 제공하는 방식으로 진행하였습니다.
Build Variant 탭에서 플레이버를 Prod로 변경하면 아래와 같이 Active되는 플레이버에 맞는 폴더가 자동으로 표시됩니다.
Prod 환경에서는 Retrofit을 사용하여 실제 서버 데이터에 접근하는 클래스를 구현했고
Demo 환경에서는 실제 서버 대신 샘플 데이터를 사용하도록 설계하였습니다.
다음으로 Build Type을 어떤 방식으로 적용했는지 설명드리겠습니다.
저는 app 모듈의 build.gradle에 아래 처럼 정의해 두었습니다.
다른 모듈에는 명시하지 않았는데 그 이유는 app모듈에 다른 모듈들이 전부 의존적이기 때문에 app 모듈이 의존하는 모든 모듈들도 동일한 buildType
으로 컴파일됩니다.
하위 라이브러리 모듈에는 기본적인 debug
와 release
두 가지 buildType
만으로 충분하다 생각되어 따로 추가하지 않았습니다.
applicationIdSuffix
: 앱의 패키지 이름에 .DEBUG
접미사를 추가합니다. 이렇게 하면 동일한 기기에 debug 및 release 버전의 앱을 동시에 설치할 수 있습니다.isMinifyEnabled
: 코드 축소를 활성화하여 APK 크기를 줄입니다.applicationIdSuffix
: 앱의 패키지 이름에 .RELEASE
접미사를 추가합니다.proguardFiles
: ProGuard 설정 파일을 지정하여 코드 난독화 및 최적화를 수행합니다.signingConfig
: 앱 서명 설정을 지정합니다. 여기서는 debug 서명 키를 사용하도록 설정되어 있습니다. 실제 배포를 위해서는 별도의 서명 키가 필요합니다.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을 구분하여 여러분의 목적에 맞게 효율적으로 개발할 수 있을 것입니다.
개인적으로 이 개념들은 복잡하다기보다는 흥미로웠습니다. 공부하는 동안 재미를 느꼈습니다.
마지막까지 읽어주셔서 감사합니다.