buildType × productFlavor 조합으로 하나의 코드베이스에서 여러 산출물을 만든다.
android {
buildTypes {
debug { applicationIdSuffix = ".debug"; isMinifyEnabled = false }
release { isMinifyEnabled = true; signingConfig = signingConfigs.getByName("release") }
}
flavorDimensions += "env"
productFlavors {
create("dev") { dimension = "env"; buildConfigField("String", "BASE_URL", "\"https://dev.api\"") }
create("prod") { dimension = "env"; buildConfigField("String", "BASE_URL", "\"https://api\"") }
}
}
변형 = flavor × buildType
- devDebug · devRelease · prodDebug · prodRelease
왜 applicationIdSuffix인가
debug 에 .debug 를 붙이면 패키지가 달라진다
- 개발 빌드와 스토어 버전을 한 기기에 동시에 설치할 수 있다
- QA 가 두 버전을 비교하며 테스트한다
주의: 패키지가 바뀌면 Google 로그인 · 지도 · FCM 설정도 그 패키지로 따로 등록해야 한다
소스셋으로 구현을 갈아 끼운다
- src/main/java/ — 공통
- src/dev/java/ — dev 전용 (디버그 메뉴 · 로그 인터셉터)
- src/prod/java/ — prod 전용
- src/debug/res/ — 디버그 아이콘 · 문자열
같은 경로·같은 클래스명을 두면 변형에 따라 다른 구현이 컴파일된다 (main 과 flavor 에 동시에 같은 클래스를 두면 충돌이다 — 한쪽에만 둔다)
BuildConfig로 분기한다
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
client.addInterceptor(HttpLoggingInterceptor().setLevel(BODY))
}
BuildConfig.DEBUG 는 상수라 R8 이 릴리스에서 이 분기를 통째로 지운다
- 로깅 코드가 배포본에 남지 않는다
AGP 8+ 에서는 buildFeatures { buildConfig = true } 를 켜야 한다
서명 정보를 커밋하지 않는다
val props = Properties().apply {
rootProject.file("keystore.properties").takeIf { it.exists() }?.let { load(it.inputStream()) }
}
signingConfigs.create("release") {
storeFile = file(props.getProperty("storeFile") ?: "")
storePassword = props.getProperty("storePassword")
}
keystore.properties 와 .jks 는 .gitignore 에 넣는다 CI 에서는 시크릿 환경변수로 주입한다
면접 함정
- ❌ "flavor를 늘려도 빌드 시간은 그대로" → 변형 수만큼 산출물이 늘어난다. 조합이 곱셈으로 증가한다.
- ❌ "debug 빌드로 성능을 측정한다" → R8이 꺼져 있고 디버그 정보가 붙어 있어 의미 없는 수치다.