🎯 사이드 프로젝트 환경설정 자동화
사이드 프로젝트를 진행하면서 개발, 테스트, 릴리즈 단계별로
번거롭게 바꾸던 설정들을 한 번에 관리하고 싶어졌습니다.
그래서 앱에 productFlavors를 도입해
🌐 환경별 API 엔드포인트, 🎨 앱 아이콘, 📦 패키지명 등을
간편하게 분리할 수 있도록 만들었어요!
Android Gradle의 productFlavors(플레이버)는
하나의 프로젝트에서 서로 다른 “버전”(환경, 기능, 배포 채널 등)을
한 번에 관리할 수 있게 해 주는 기능입니다.
dev), 스테이징(staging), 프로덕션(prod) 등src/dev/java/..., src/prod/java/... 폴더에src/dev/res/..., src/prod/res/... 에서buildConfigField, manifestPlaceholders, applicationIdSuffix 등을BuildConfig.FLAVOR로 분기 처리 android {
flavorDimensions += "environment"
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
buildConfigField("String", "BASE_URL", "\"https://dev.example.com\"")
}
create("prod") {
dimension = "environment"
buildConfigField("String", "BASE_URL", "\"https://api.example.com\"")
}
}
}
| Variant | 패키지명 | BASE_URL |
|---|---|---|
| devDebug | com.example.app.dev | "https://dev.example.com" |
| prodRelease | com.example.app | "https://api.example.com" |
Clean Architecture 기반으로 프로젝트를 구성하다 보니, 수십 개의 feature, domain, data 모듈이 존재합니다. 하지만 productFlavors는 Android 설정이기 때문에 매번 각 모듈의 build.gradle.kts에 동일한 Flavor 블록을 복붙해야 합니다.
💥 반복 작업: 모든 Android 모듈에 flavorDimensions, productFlavors { … } 선언
💥 유지 보수 어려움: 새 모듈을 추가하면 모듈 수만큼 buildConfigField를 일일이 수정
💥 휴먼 에러 가능성: 매번 수작업으로 동일한 블록을 복붙하거나 수정하면서 누락·오타 등의 실수를 저지를 수 있음
내 프로젝트도 Clean Architecture 기반이라 모듈 수가 방대했고, 매번 productFlavors 설정을 복붙하는 건 시간 낭비이자 휴먼 에러의 원인이었습니다.
그래서 이 반복 작업을 완전히 자동화할 수 있는 Convention Plugin을 도입하기로 했어요.
applicationIdSuffix, buildConfigField, dimension 등을 플러그인이 대신 세팅다음과 같이 FlavorDimension과 ImdangFlavor enum, 그리고 모든 Android 모듈에 공통으로 flavorDimensions와 productFlavors를 적용해 주는 configureFlavors 함수를 정의해 둡니다.
@Suppress("EnumEntryName")
enum class FlavorDimension {
server
}
@Suppress("EnumEntryName")
enum class ImdangFlavor(
val dimension: FlavorDimension,
val applicationIdSuffix: String? = null
) {
dev(FlavorDimension.server, applicationIdSuffix = ".dev"),
product(FlavorDimension.server),
}
fun configureFlavors(
commonExtension: CommonExtension<*, *, *, *, *, *>,
flavorConfigurationBlock: ProductFlavor.(flavor: ImdangFlavor) -> Unit = {}
) {
commonExtension.apply {
// 모든 dimension 등록
FlavorDimension.values().forEach { flavorDimensions += it.name }
productFlavors {
// ImdangFlavor enum을 순회하며 flavor 생성
ImdangFlavor.values().forEach { imdangFlavor ->
register(imdangFlavor.name) {
dimension = imdangFlavor.dimension.name
flavorConfigurationBlock(this, imdangFlavor)
// Application 모듈인 경우 applicationIdSuffix 자동 지정
if (this@apply is ApplicationExtension &&
this is ApplicationProductFlavor &&
imdangFlavor.applicationIdSuffix != null
) {
applicationIdSuffix = imdangFlavor.applicationIdSuffix
}
}
}
}
}
}
dimension과 applicationIdSuffix를 한곳에서 선언FlavorDimension enum에 정의된 모든 dimension 자동 등록 ImdangFlavor enum 순회하며 flavor 생성 flavorConfigurationBlock으로 추가 설정 가능 applicationIdSuffix까지 자동 반영Flavor 설정을 반복하지 않도록, 각 모듈에서 공통적으로 configureFlavors()를 적용할 수 있는
Convention Plugin 구조를 만들었습니다.
Application 모듈 ApplicationExtension에 configureFlavors(this) 호출하여 Flavor 자동 구성class AndroidApplicationFlavorsConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
extensions.configure<ApplicationExtension> {
configureFlavors(this)
}
}
}
}
Library 모듈 configureKotlinAndroid) configureFlavors(this) 호출로 Flavor 구성 class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.library")
apply(plugin = "org.jetbrains.kotlin.android")
extensions.configure<LibraryExtension> {
configureKotlinAndroid(this)
defaultConfig.targetSdk = Config.targetSdk
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testOptions.animationsDisabled = true
configureFlavors(this)
}
dependencies {
"androidTestImplementation"(libs.findLibrary("kotlin.test").get())
"testImplementation"(libs.findLibrary("kotlin.test").get())
}
}
}
}
✅ 공통 적용: 모든 Library 모듈이 AndroidLibraryConventionPlugin을 사용하므로
configureFlavors()가 자동으로 공통 적용됨
✂️ 반복 제거: 각 모듈의 build.gradle.kts가 간결하고 깔끔해짐
🛠️ 유지보수 효율 향상: Flavor 설정을 바꿀 때 플러그인 코드만 수정하면
전체 모듈에 한 번에 반영됨
Convention Plugin을 통해 flavorDimensions와 productFlavors를 공통 관리하는 건 아주 효율적이었다.
하지만…
❗ 문제 발생:
공통으로 선언한buildConfigField외에도
각 모듈마다 독립적으로 필요한 설정값을 추가하고 싶은 경우엔 어떻게 해야 할까?
예를 들어,
dev에서는 특정 모듈에서만 사용하는 API 키를 넣고 싶다거나 product에서는 어떤 기능 토글 값을 모듈마다 다르게 설정하고 싶을 수 있다 configureFlavorSettings() 함수 추가각 모듈의 build.gradle.kts에서 필요한 buildConfigField를 추가로 선언할 수 있도록,
다음과 같은 확장 함수를 정의했다:
// 각 모듈의 build.gradle.kts 파일에서 필요한 configuration을 적용할 수 있도록
fun configureFlavorSettings(
commonExtension: CommonExtension<*, *, *, *, *, *>,
flavorConfigBlock: ProductFlavor.(flavor: ImdangFlavor) -> Unit
) {
commonExtension.productFlavors {
ImdangFlavor.values().forEach { imdangFlavor ->
named(imdangFlavor.name) {
flavorConfigBlock(this, imdangFlavor)
}
}
}
}
configureFlavors() 함수는 Flavor 선언 및 기본 설정을 담당하고 configureFlavorSettings() 함수는 모듈별로 개별적인 설정을 확장하는 용도로 사용됨 📌 즉, 공통 설정과 모듈별 설정을 역할 분리하여 유연하게 관리 가능!
build.gradle.kts)configureFlavorSettings(this) { flavor ->
when (flavor) {
ImdangFlavor.dev -> {
addManifestPlaceholders(mapOf("KAKAO_NATIVE_KEY" to DevConfig.KAKAO_NATIVE_KEY))
addManifestPlaceholders(mapOf("NAVER_CLIENT_ID" to DevConfig.NAVER_CLIENT_ID))
buildConfigField(
"String",
"KAKAO_NATIVE_KEY",
"\"${DevConfig.KAKAO_NATIVE_KEY}\""
)
buildConfigField(
"String",
"GOOGLE_WEB_CLIENT_ID",
"\"${DevConfig.GOOGLE_WEB_CLIENT_ID}\""
)
}
ImdangFlavor.product -> {
addManifestPlaceholders(mapOf("KAKAO_NATIVE_KEY" to ProductConfig.KAKAO_NATIVE_KEY))
addManifestPlaceholders(mapOf("NAVER_CLIENT_ID" to ProductConfig.NAVER_CLIENT_ID))
buildConfigField(
"String",
"KAKAO_NATIVE_KEY",
"\"${ProductConfig.KAKAO_NATIVE_KEY}\""
)
buildConfigField(
"String",
"GOOGLE_WEB_CLIENT_ID",
"\"${ProductConfig.GOOGLE_WEB_CLIENT_ID}\""
)
}
}
}
configureFlavors()는 전 모듈에 공통 적용 configureFlavorSettings()는 개별 모듈에 맞춘 설정 적용 이번 프로젝트에서는 모듈 수가 많은 Clean Architecture 구조에서
productFlavors를 효율적이고 일관되게 관리하기 위한 방법으로
👉 Convention Plugin 기반의 Flavor 설정 자동화 구조를 구축했습니다.
덕분에,
build.gradle.kts의 가독성과 유지보수성도 훨씬 좋아졌습니다.이제 새로운 모듈이 추가되더라도, Convention Plugin을 통해 자동으로 Flavor가 적용되는 구조가 되었기 때문에
🚀 사소한 반복도 자동화하고
🛠️ 환경 설정을 체계적으로 관리해
더 이상 build.gradle.kts를 복붙하며 고생하지 말고,
✨ 진짜 중요한 비즈니스 로직 개발에 집중해보세요.