Flavor 설정 반복작업, 이렇게 끝냈습니다 🔧

이진영·2025년 7월 16일

🎯 사이드 프로젝트 환경설정 자동화
사이드 프로젝트를 진행하면서 개발, 테스트, 릴리즈 단계별로
번거롭게 바꾸던 설정들을 한 번에 관리하고 싶어졌습니다.
그래서 앱에 productFlavors를 도입해
🌐 환경별 API 엔드포인트, 🎨 앱 아이콘, 📦 패키지명 등을
간편하게 분리할 수 있도록 만들었어요!


🌟 Flavor란?

Android Gradle의 productFlavors(플레이버)는
하나의 프로젝트에서 서로 다른 “버전”(환경, 기능, 배포 채널 등)을
한 번에 관리할 수 있게 해 주는 기능입니다.


🎯 목적

  • 🔧 환경 분리
    • 개발(dev), 스테이징(staging), 프로덕션(prod) 등
      환경별로 서로 다른 설정(API URL, 로깅 레벨, 기능 토글 등)을 분리
  • 💼 에디션 관리
    • 무료/유료, B2B/B2C 등 기능별 에디션을 동일 코드베이스에서 빌드

✨ 주요 이점

  1. 🗂️ 코드 분리
    • src/dev/java/..., src/prod/java/... 폴더에
      flavor별 고유 로직을 배치할 수 있어요.
  2. 🎨 리소스 분리
    • src/dev/res/..., src/prod/res/... 에서
      아이콘·문구·색상 등을 환경별 차별화!
  3. 🛠️ BuildConfig & Manifest 설정
    • buildConfigField, manifestPlaceholders, applicationIdSuffix 등을
      flavor별로 지정하여 코드 내에서 BuildConfig.FLAVOR로 분기 처리
  4. 🚀 빌드 자동화 & CI 연동
    • Fastlane · Gradle Play Publisher 등과 연계해
      flavor별 배포 트랙(내부테스트·프로덕션)을 자동화

🛠️ 간단 예시 (Kotlin DSL)

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
devDebugcom.example.app.dev"https://dev.example.com"
prodReleasecom.example.app"https://api.example.com"

🚩문제점

🏛️ Clean Architecture 프로젝트에서의 번거로움

Clean Architecture 기반으로 프로젝트를 구성하다 보니, 수십 개의 feature, domain, data 모듈이 존재합니다. 하지만 productFlavors는 Android 설정이기 때문에 매번 각 모듈의 build.gradle.kts에 동일한 Flavor 블록을 복붙해야 합니다.

💥 반복 작업: 모든 Android 모듈에 flavorDimensions, productFlavors { … } 선언
💥 유지 보수 어려움: 새 모듈을 추가하면 모듈 수만큼 buildConfigField를 일일이 수정
💥 휴먼 에러 가능성: 매번 수작업으로 동일한 블록을 복붙하거나 수정하면서 누락·오타 등의 실수를 저지를 수 있음


💡 해결 방법: Convention Plugin 적용

내 프로젝트도 Clean Architecture 기반이라 모듈 수가 방대했고, 매번 productFlavors 설정을 복붙하는 건 시간 낭비이자 휴먼 에러의 원인이었습니다.
그래서 이 반복 작업을 완전히 자동화할 수 있는 Convention Plugin을 도입하기로 했어요.

🎯 주요 목표

  • 🧩 일관된 구성 유지
    하나의 플러그인만 적용하면 모든 모듈에 동일한 Flavor 설정이 자동으로 반영
  • ⚙️ 자동화로 효율 극대화
    applicationIdSuffix, buildConfigField, dimension 등을 플러그인이 대신 세팅
  • 🛡️ 유지 보수성 향상
    Flavor 추가·수정 시 플러그인 코드만 고치면 전체 모듈에 일괄 적용

🧑‍💻 Flavor 선언 및 공통 설정 함수

다음과 같이 FlavorDimensionImdangFlavor enum, 그리고 모든 Android 모듈에 공통으로 flavorDimensionsproductFlavors를 적용해 주는 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
                    }
                }
            }
        }
    }
}

🔍 ImdangFlavor

  • 각 flavor의 dimensionapplicationIdSuffix를 한곳에서 선언

🔍 configureFlavors

  • FlavorDimension enum에 정의된 모든 dimension 자동 등록
  • ImdangFlavor enum 순회하며 flavor 생성
  • 필요 시 flavorConfigurationBlock으로 추가 설정 가능
  • Application 모듈에는 applicationIdSuffix까지 자동 반영

🔌 Convention Plugin 구현

Flavor 설정을 반복하지 않도록, 각 모듈에서 공통적으로 configureFlavors()를 적용할 수 있는
Convention Plugin 구조를 만들었습니다.


📱 AndroidApplicationFlavorsConventionPlugin

  • 🧩 적용 대상: Application 모듈
  • ⚙️ 역할: ApplicationExtensionconfigureFlavors(this) 호출하여 Flavor 자동 구성
class AndroidApplicationFlavorsConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            extensions.configure<ApplicationExtension> {
                configureFlavors(this)
            }
        }
    }
}

📚 AndroidLibraryConventionPlugin

  • 🧩 적용 대상: Library 모듈
  • ⚙️ 역할:
    • Android 라이브러리 기본 설정
    • Kotlin Android 공통 설정 (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 설정을 바꿀 때 플러그인 코드만 수정하면
    전체 모듈에 한 번에 반영됨


🧩 커스텀 BuildConfigField 문제 해결

😅 그런데 문제가 생겼다

Convention Plugin을 통해 flavorDimensionsproductFlavors공통 관리하는 건 아주 효율적이었다.
하지만…

문제 발생:
공통으로 선언한 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 설정 자동화 구조를 구축했습니다.

덕분에,

  • 💡 모든 모듈에 공통적으로 Flavor 설정을 적용할 수 있고
  • ⚙️ 개별 모듈의 설정도 유연하게 확장할 수 있으며
  • 🧼 build.gradle.kts가독성과 유지보수성도 훨씬 좋아졌습니다.

이제 새로운 모듈이 추가되더라도, Convention Plugin을 통해 자동으로 Flavor가 적용되는 구조가 되었기 때문에

  • ✅ 개발 생산성도 높아지고,
  • ✅ 환경별 관리도 안정적으로 유지할 수 있게 되었어요.

🚀 사소한 반복도 자동화하고
🛠️ 환경 설정을 체계적으로 관리
더 이상 build.gradle.kts를 복붙하며 고생하지 말고,
✨ 진짜 중요한 비즈니스 로직 개발에 집중해보세요.

profile
Android Developer

0개의 댓글