Android 개발에서 Product Flavor는 공유 코드 베이스에서 동일한 애플리케이션의 여러 변형을 생성할 수 있는 Gradle 메커니즘입니다. 각 flavor는 고유한 applicationId, 리소스, 종속성 및 기능을 가질 수 있습니다. 예를 들어, 무료 버전과 유료 버전이 있습니다. Google Android Developers, 2025에 따르면, Product Flavors는 Build Variants 시스템의 일부이며 flavorDimensions를 통해 Build Types와 결합됩니다. 이는 Google Play에서 여러 앱 버전을 게시하는 표준 접근 방식입니다.
핵심 포인트
Product Flavor는 android.productFlavors 블록 내의 Gradle 구성으로, 제품 변형을 설명합니다. 각 flavor는 applicationId, versionName, versionCode, minSdkVersion, targetSdkVersion, signingConfig 및 defaultConfig의 기타 매개변수를 재정의할 수 있습니다. Product Flavors에는 수량 제한이 없습니다. 프로젝트에 2개, 5개 또는 10개의 flavor가 포함될 수 있으며, Gradle이 모든 조합을 처리합니다.
Product Flavor는 코드베이스 재사용(codebase reuse) 문제를 해결합니다. 즉, 단일 저장소에서 여러 다른 애플리케이션을 빌드해야 하는 경우입니다. 일반적인 시나리오: 광고가 있는 무료 버전과 광고가 없는 유료 버전, 제한된 기능의 데모 버전, 기업용 및 소비자용 버전, 다양한 클라이언트를 위한 화이트 라벨 앱. Product Flavors가 없으면 각 버전을 별도의 프로젝트로 유지 관리해야 하며, 60-70%의 코드 중복이 발생합니다.
역사적으로 Product Flavors는 Android Gradle Plugin 0.9(2013)에서 ant 구성을 대체하기 위해 등장했습니다. 그 이전에는 개발자가 다른 버전에 대해 별도의 프로젝트를 사용하거나 빌드 전에 수동으로 리소스를 교체했습니다. AGP에 flavor가 도입되면서 접근 방식이 통합되고 표준이 되었습니다. JetBrains, 2024의 조사에 따르면, 여러 버전이 있는 Android 프로젝트의 78%가 Product Flavors를 사용하고 있으며, 나머지는 BuildConfig 또는 리플렉션을 통한 수동 전환을 사용합니다.
Build Type은 빌드 프로세스(디버깅이 있는 debug, 최적화가 있는 release)를 관리합니다. Product Flavor는 빌드 콘텐츠(유료 기능이 없는 free, 유료 기능이 있는 paid)를 관리합니다. Build Type은 인프라 설정이고, Product Flavor는 제품 설정입니다. 두 개념은 직교합니다. free flavor의 debug 빌드는 free flavor의 release 빌드와 컴파일 매개변수만 다를 뿐 기능은 다르지 않습니다. Product Flavor를 사용하여 디버거를 비활성화할 수 없습니다. 이는 Build Type의 역할입니다.
Flavor Dimensions는 Product Flavors를 독립적인 범주로 그룹화하는 메커니즘입니다. 앱에 무료/유료 버전이 있고 별도로 미국/유럽 지역이 있는 경우, flavor는 두 가지 차원으로 그룹화됩니다: "tier"(free, paid)와 "region"(us, eu). Gradle은 차원의 데카르트 곱을 생성합니다: freeUs, freeEu, paidUs, paidEu — 4개의 변형. 차원이 없으면 Gradle은 4개의 모든 flavor를 단일 평면으로 처리하고 하나만 선택할 수 있습니다.
차원은 flavorDimensions 블록에서 문자열 또는 문자열 목록으로 선언됩니다. 차원의 순서는 source set의 우선순위에 영향을 줍니다. 첫 번째 차원이 가장 높은 우선순위를 갖습니다. 차원 A(tier)가 먼저 지정되면 리소스 충돌 시 src/free/가 src/us/를 재정의합니다. 순서는 Variant 이름이 형성되는 방식에도 영향을 줍니다. 첫 번째 차원의 flavor가 먼저 오고, 그 다음 두 번째 차원, 그 다음 Build Type 순서입니다: freeUsDebug.
차원 수에는 제한이 없지만, 각 새 차원은 Build Variants 수를 곱합니다. 4개의 차원(각각 2개의 flavor)과 2개의 build types가 있는 프로젝트의 경우 2 × 2 × 2 × 2 × 2 = 32개의 변형이 생깁니다. 실용적인 한계는 3차원(최대 8-12개 변형)입니다. 그 이상이면 Gradle 구성이 느려지고 Android Studio의 Build Variants 패널을 읽을 수 없게 됩니다.
android {
flavorDimensions "tier", "api"
productFlavors {
free {
dimension "tier"
applicationId "com.example.app.free"
versionNameSuffix "-free"
}
paid {
dimension "tier"
applicationId "com.example.app.paid"
}
minApi21 {
dimension "api"
minSdk 21
}
minApi26 {
dimension "api"
minSdk 26
}
}
}
// 결과: freeMinApi21, freeMinApi26, paidMinApi21, paidMinApi26
// 각 × debug/release = 8개의 Build Variants
Product Flavor를 생성하려면 android 내에 productFlavors 블록을 추가하고 flavor 이름과 매개변수를 지정해야 합니다. 최소 flavor 선언은 이름과 차원입니다. 다른 모든 매개변수는 defaultConfig에서 상속되며 재정의할 수 있습니다. flavor는 applicationId, versionCode, testInstrumentationRunner를 포함하여 defaultConfig를 완전히 상속합니다.
각 flavor는 applicationId를 재정의할 수 있습니다. 이를 통해 동일한 기기에 여러 앱 버전을 동시에 설치할 수 있습니다. 예를 들어, 무료 버전은 com.example.app.free, 유료 버전은 com.example.app.paid가 됩니다. applicationId가 재정의되지 않으면 모든 flavor가 동일한 식별자를 가지며 나란히 설치할 수 없습니다. applicationId는 매니페스트의 패키지와 일치해야 합니다(applicationIdSuffix를 사용하지 않는 경우).
AGP 8+는 build.gradle에 Groovy 대신 Kotlin DSL을 사용할 것을 권장합니다. Kotlin DSL은 구성에 대한 타입 안전 접근을 제공합니다. IDE가 매개변수 이름을 제안하고, 컴파일 타임에 타입을 확인하며, 오류를 강조 표시합니다. Product Flavors를 위한 Groovy에서 Kotlin DSL로의 마이그레이션은 일반적으로 따옴표를 괄호로 바꾸고 타입을 추가하는 것으로 구성됩니다. AGP는 하위 호환됩니다. 두 구문 모두 동일한 프로젝트 내에서 병렬로 작동합니다.
// build.gradle.kts — Kotlin DSL
android {
flavorDimensions += "tier"
productFlavors {
register("free") {
dimension = "tier"
applicationId = "com.example.app.free"
versionNameSuffix = "-free"
buildConfigField("boolean", "IS_PREMIUM", "false")
}
register("paid") {
dimension = "tier"
applicationId = "com.example.app.paid"
versionNameSuffix = "-paid"
buildConfigField("boolean", "IS_PREMIUM", "true")
}
}
}
각 Product Flavor는 자체 source set — src/<flavorName>/ 디렉토리를 생성합니다. 이 디렉토리에는 재정의된 리소스, 소스 파일 및 매니페스트가 포함될 수 있습니다. flavor source set은 main 위의 오버레이 역할을 합니다. src/free/res/의 파일은 같은 이름의 src/main/res/ 파일을 재정의합니다. 이를 통해 메인 코드를 수정하지 않고 각 flavor에 대해 다른 문자열, 아이콘, 색상 및 레이아웃을 가질 수 있습니다.
Java/Kotlin 클래스를 재정의하는 두 가지 접근 방식이 있습니다: flavor별 구현(각 flavor에서 추상 클래스 구현)과 BuildConfig 필드(코드 분기)입니다. 첫 번째 접근 방식이 더 깔끔합니다. main에서 인터페이스나 추상 클래스를 정의하고 src/free/ 및 src/paid/에 구체적인 구현을 배치합니다. 빌드 중에는 현재 flavor의 구현만 컴파일됩니다. 이는 APK 크기 감소(유료 코드가 무료 버전에 들어가지 않음)와 보안(실수로 유료 함수를 호출할 수 없음)이라는 동시 이점을 제공합니다.
flavor source set의 AndroidManifest.xml은 대체하지 않고 메인 매니페스트와 병합됩니다. 병합은 Android 규칙을 따릅니다. 동일한 요소의 중복 속성은 재정의되고 고유 속성은 추가됩니다. 예를 들어, 메인 매니페스트가 INTERNET 권한을 선언하고 free가 선언하지 않으면 인터넷 권한이 유지됩니다. 그러나 tools:node="replace"를 사용하면 특정 flavor의 전체 매니페스트 블록을 대체할 수 있습니다. 이는 다른 flavor에 다른 권한이 필요한 경우(유료 버전의 SD 카드 쓰기, 무료 버전의 카메라) 유용합니다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:label="Free App"
tools:replace="android:label">
</application>
</manifest>
일반적인 시나리오를 고려해 보겠습니다. free — 광고와 기본 기능이 있는 버전, paid — 광고 없이 확장된 기능이 있는 버전. 무료 버전의 경우 applicationId가 "com.example.app.free"로 설정되고, 유료 버전은 "com.example.app.paid"로 설정됩니다. applicationId는 Android 시스템에서 애플리케이션의 고유 식별자이므로 두 버전을 동일한 기기에 동시에 설치할 수 있습니다.
아키텍처적으로 분리는 인터페이스 + flavor 구현을 통해 구축됩니다. 메인 source set에서 PaymentService 인터페이스가 선언됩니다. src/free/에는 AdMob을 통해 결제 전에 광고를 표시하는 구현이 있습니다. src/paid/에는 결제 게이트웨이로 직접 진행하는 구현이 있습니다. PaymentService를 사용하는 코드는 어떤 구현이 로드되었는지 알지 못합니다. 이는 컴파일 타임에 결정됩니다. 이 접근 방식은 개발자가 실수로 호출하더라도 구독 관리 코드가 무료 버전에 들어가지 않도록 보장합니다.
종속성 포함/제외로 인해 다른 flavor의 APK 크기는 5-15MB까지 차이가 날 수 있습니다. 특정 flavor에서 라이브러리를 제외하려면 build.gradle에서 flavor별 종속성을 사용합니다: freeImplementation 'com.google.android.gms:play-services-ads:23.0.0'. 이 종속성은 free 변형에만 추가되며 유료 버전의 크기를 증가시키지 않습니다. 공유 종속성에는 implementation을 사용합니다. 모든 flavor가 이들을 포함합니다.
// src/main/kotlin/com/example/payment/PaymentService.kt
interface PaymentService {
fun processOrder(amount: Double, callback: (PaymentResult) -> Unit)
}
// src/free/kotlin/.../FreePaymentService.kt
class FreePaymentService : PaymentService {
override fun processOrder(amount: Double, callback: (PaymentResult) -> Unit) {
AdManager.showInterstitial {
PaymentGateway.charge(amount, callback)
}
}
}
// src/paid/kotlin/.../PaidPaymentService.kt
class PaidPaymentService : PaymentService {
override fun processOrder(amount: Double, callback: (PaymentResult) -> Unit) {
PaymentGateway.charge(amount, callback)
}
}
멀티모듈 프로젝트에서 라이브러리 모듈에는 자체 Product Flavors가 없을 수 있으며, 이는 문제를 만듭니다. 라이브러리는 한 번(release로) 빌드되지만, flavor가 있는 앱 모듈은 해당 변형의 라이브러리를 기대합니다. AGP 8.1부터 라이브러리는 publishing.multipleVariants 블록을 통해 여러 변형을 게시할 수 있습니다. 이를 통해 라이브러리의 모든 flavor 변형을 단일 maven 저장소에 게시할 수 있으며, 앱 모듈이 자동으로 올바른 것을 선택합니다.
대안적인 접근 방식은 라이브러리에 앱 모듈과 동일한 flavorDimensions 및 productFlavors를 선언하는 것입니다. AGP는 하나의 차원 내에서 정확한 이름 일치를 통해 flavor를 자동으로 매칭합니다. 라이브러리의 flavor 이름이 앱의 이름과 일치하면 AGP는 일관된 변형을 생성합니다. 유지 관리를 용이하게 하기 위해 공통 flavor 정의를 Convention Plugin으로 추출하는 것이 좋습니다. 이는 프로젝트의 모든 모듈에 적용되는 Gradle 플러그인입니다.
게시를 목적으로 하지 않는 라이브러리(내부 모듈)의 경우 루트 프로젝트의 build.gradle을 통해 flavor를 동기화하는 것으로 충분합니다. Gradle은 모든 하위 프로젝트에 구성을 적용할 수 있는 subprojects 메서드를 제공합니다. 그러나 subprojects에 너무 많은 구성이 있으면 구성 단계가 느려집니다. Convention Plugins을 사용하는 것이 좋습니다. 이들은 한 번 컴파일되고 재사용되어 구성 시간을 15-30% 단축합니다.
자주 묻는 질문
수량 제한은 없지만, 각 차원이 Build Variants 수를 곱합니다. 하나의 차원에 4개의 flavor + 2개의 build types = 8개의 변형입니다. 두 개의 차원에 4 + 4 = 16개의 변형입니다. 3개 이하의 차원과 총 10-12개의 변형을 사용하는 것이 좋습니다.
네, source set src/<flavor>/AndroidManifest.xml을 통해 가능합니다. 매니페스트는 메인과 병합됩니다. 전체 블록을 대체하려면 tools:node="replace"를 사용하세요. 예를 들어, 특정 flavor의 앱 레이블이나 권한을 변경할 수 있습니다.
<flavorName>Implementation 구성을 사용하세요. 예: freeImplementation 'com.google.android.gms:play-services-ads:23.0.0'. 이 종속성은 free 변형을 빌드할 때만 포함됩니다. 유료의 경우: paidImplementation. 공통 종속성은 implementation을 통해 지정됩니다.
Product Flavor는 제품 버전(free, paid, demo)을 정의하고, Build Type은 빌드 방법(debug, release)을 정의합니다. flavor는 applicationId, versionName, 리소스를 재정의할 수 있습니다. Build Type은 debuggable, minification, signing을 제어합니다. 둘은 직교하며 Build Variant로 결합됩니다.
네, Product Flavors는 제한 없이 Compose와 작동합니다. 다른 flavor는 source sets 또는 추상 클래스 구현을 통해 다른 Compose 화면을 가질 수 있습니다. flavor별 Compose 종속성을 추가할 수도 있습니다: freeImplementation 'androidx.compose.ui:ui-tooling'.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.