AGP 9.0.0이 정식 릴리즈 되었습니다. 버저닝을 적용할때까지만해도 적당히 한두줄 수정하면 되겠지 하고 업데이트를 시작했는데요..
무수한 빨간줄의 요청으로 빌드 시스템을 다시 설계하면서 겪은 경험을 공유하고자 합니다.
달라진 것은 생각보다 많았다
AGP 9.0은 AGP 8.0 이후 2년 만의 메이저 릴리즈입니다. 단순한 기능 추가가 아니라 지난 몇 년간 준비해온 구조적 변화를 본격적으로 적용한 버전이었습니다.
새로운 DSL이 기본값이 되다
AGP 7.x와 8.x에서는 하위 호환성 유지를 위해 구형 DSL 타입(BaseExtension 등)과 신형 공개 인터페이스를 동시에 지원했어요. 그런데 AGP 9.0부터는 새로운 DSL 인터페이스만 사용하며 내부 구현이 완전히 숨겨진 새로운 타입으로 변경되었습니다.
즉 android.newDsl=true가 기본값이 되면서 구형 DSL 인터페이스는 더 이상 사용할 수 없습니다. android.newDsl=false로 임시 회피할 수 있지만 AGP 10.0(2026년 중반 예정)에서는 이마저도 불가능해집니다.
CommonExtension의 변화가 가져온 파장
가장 큰 영향을 미친 변화는 CommonExtension의 제네릭 타입 파라미터 제거입니다. 공식 문서에 따르면 미래의 소스 레벨 호환성 문제를 방지하기 위한 변경이지만 그 여파는 작지 않았습니다.
기존에는 와일드카드 타입으로 CommonExtension<*, *, *, *, *, *>를 선언하면 buildTypes, defaultConfig 같은 블록에 접근할 수 있었어요. 하지만 AGP 9.0부터는 이런 블록 메서드들이 ApplicationExtension, LibraryExtension, DynamicFeatureExtension 등 각 구체 타입으로 옮겨갔습니다.
왜 이런 변경을 단행했을까요? 정확한 이유는 공식 문서에 명시되어 있지 않지만 몇 가지 추측해볼 수 있습니다.
타입 안전성 강화
Application과 Library는 근본적으로 다른 산출물을 만듭니다. 전자는 APK를 생성하고 applicationId를 가지지만 후자는 AAR을 생성하며 applicationId가 없어요. 이런 차이를 제네릭으로 추상화하다 보면 컴파일 타임에 잡아낼 수 있는 오류가 런타임으로 미뤄질 위험이 있습니다.
API 변경의 여파 최소화
제네릭 타입 파라미터가 6개나 되는 인터페이스는 유지보수가 어렵습니다. 타입 파라미터를 완전히 제거함으로써 향후 API 변경의 여파를 최소화하려는 의도로 보입니다.🤔
책임 분리의 명확화
Application과 Library가 각자의 설정 블록을 갖게 되면서 "이 설정은 어디에 속하는가?"가 더 명확해집니다. 이런 변경은 개발자들이 빌드 로직을 이해하고 작성하는 데 도움이 됩니다.
내부 API 접근 차단
AGP 9.0은 또한 내부 클래스에 대한 접근을 완전히 차단했어요. 기존에 많은 서드파티 플러그인이 BaseExtension 같은 내부 타입에 의존하고 있었는데 이제는 공개 인터페이스만 사용해야 합니다. 이 변화로 인해 Paparazzi, 일부 버전의 Firebase 플러그인 등이 호환성 문제를 겪었고 플러그인 생태계 전체가 업데이트되어야 했습니다.
나도 문제다
Hilingual 프로젝트는 Convention Plugin 패턴을 사용해 빌드 로직을 관리하고 있습니다. build-logic 모듈에서 Application과 Library 모듈의 공통 설정을 다루는 방식인데요.
AGP 8.x에서는 이렇게 작성할 수 있었습니다.
// 기존 코드 (AGP 8.x)
fun Project.configureBuildTypes(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
buildFeatures {
buildConfig = true
}
buildTypes {
debug {
buildConfigField("String", "BASE_URL", "\"https://dev.api.example.com\"")
}
release {
buildConfigField("String", "BASE_URL", "\"https://api.example.com\"")
}
}
}
}
제네릭 타입 파라미터를 와일드카드로 처리하면서 Application과 Library 양쪽 모두에서 사용할 수 있는 공통 함수를 만들 수 있었어요. 깔끔하고 편리합니다만..
AGP 9.0으로 업데이트하자마자 빨간줄 천지가 되었습니다.
Unresolved reference: buildTypes
CommonExtension에서 buildTypes에 접근할 수 없다는 에러입니다. 타입 파라미터가 사라지면서 이런 블록들이 각 구체 타입으로 이동했기 때문이에요.
그리고 아직 AGP 9.0을 지원하지 않는 서드파티 플러그인들도 있습니다. 대표적으로 안드로이드 생태계의 의존성 주입인 dagger-hilt가 있어요. 2.58 릴리즈에서도 AGP 9를 지원하지 않았는데요.

이 글을 작성중인 시간 1/21(수) 18:33 기준으로 8시간 전에 따끈한 업데이트가 나왔습니다.

이처럼 AGP 업그레이드는 단순히 우리 코드만 고치는 게 아니라 생태계 전체가 준비될 때까지 기다려야 하는 일이기도 합니다.
해결 방법 찾기
1. 타입 분기로 접근하기
가장 직관적인 방법은 CommonExtension을 구체적인 타입으로 분기 처리하는 것이었습니다.
fun Project.configureBuildTypes(commonExtension: CommonExtension) {
// 공통 로직을 함수로 분리
fun configureBuildTypeFields(
buildConfigField: (String, String, String) -> Unit,
isDebug: Boolean
) {
val baseUrl = if (isDebug) {
"\"https://dev.api.example.com\""
} else {
"\"https://api.example.com\""
}
buildConfigField("String", "BASE_URL", baseUrl)
buildConfigField("Boolean", "DEBUG_MODE", isDebug.toString())
}
commonExtension.apply {
when (this) {
is ApplicationExtension -> {
buildFeatures {
buildConfig = true
}
buildTypes {
getByName("debug") {
configureBuildTypeFields(::buildConfigField, true)
}
getByName("release") {
configureBuildTypeFields(::buildConfigField, false)
}
}
}
is LibraryExtension -> {
buildFeatures {
buildConfig = true
}
buildTypes {
getByName("debug") {
configureBuildTypeFields(::buildConfigField, true)
}
getByName("release") {
configureBuildTypeFields(::buildConfigField, false)
}
}
}
}
}
}
코드가 길어 보일 수 있지만 이 방식에는 몇 가지 장점이 있습니다.
타입 안전성 확보
컴파일러가 타입을 명확히 인지합니다. when 표현식 내부에서 스마트 캐스트가 일어나기 때문에 IDE의 자동완성도 정확하게 동작해요.
명시적인 구분
Application과 Library의 차이를 명시적으로 구분할 수 있습니다. 예를 들어 applicationId는 ApplicationExtension에만 존재하는 속성이에요. 이런 차이를 코드로 명확하게 표현할 수 있습니다.
재사용 가능한 로직
공통 로직은 내부 함수로 추출할 수 있어요. configureBuildTypeFields처럼 실제 값을 설정하는 로직은 재사용하면서 구조적인 설정은 각 타입별로 처리하는 방식입니다.
2. Variant API 활용하기
buildConfigField 같은 값 주입 작업은 androidComponents의 onVariants API로도 처리할 수 있습니다.
val androidComponents = extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
val baseUrl = if (variant.buildType == "debug") {
"https://dev.api.example.com"
} else {
"https://api.example.com"
}
variant.buildConfigFields?.put(
"BASE_URL",
BuildConfigField("String", "\"$baseUrl\"", "API Base URL")
)
variant.buildConfigFields?.put(
"DEBUG_MODE",
BuildConfigField("Boolean", (variant.buildType == "debug").toString(), "Debug Mode")
)
}
이 방식은 분명 깔끔합니다. 타입 분기 없이 variant 레벨에서 처리할 수 있으니까요.
하지만 문제가 있었습니다. buildFeatures { buildConfig = true }와 같은 구조적 설정은 여전히 DSL 단계에서 처리해야 합니다. 결국 when 분기를 완전히 없앨 수는 없었어요.
그리고 buildConfigFields가 nullable하기 때문에 null 체크를 계속 해야 했습니다.
val buildConfigFields: MapProperty<String, BuildConfigField<out Serializable>>?
뭘 우선시해야 할까?
두 가지 방식 사이에서 고민했습니다. Variant API가 더 모던하고 세련되어 보이긴 했지만 Hilingual 프로젝트에서는 타입 분기 방식을 선택했습니다.
이유는 간단합니다. 일관성입니다.
configureBuildTypes 함수 하나에서 DSL 설정과 값 주입을 모두 처리하는 것이 코드를 읽는 사람 입장에서 더 명확하다고 생각했어요. "빌드 타입 설정은 여기서 한다"는 것을 한눈에 알 수 있으니까요.
물론 프로젝트의 성격에 따라 다른 선택을 할 수도 있을 것입니다. 중요한 것은 어떤 방식을 선택하든 그 이유를 명확히 아는 것이라고 생각합니다.
android.newDsl=false로 미룰 수도 있긴 해요
만약 서드파티 플러그인 때문에 당장 AGP 9.0으로 넘어가기 어렵다면 gradle.properties에 다음을 추가할 수 있습니다.
android.newDsl=false
android.enableLegacyVariantApi=true
이렇게 하면 구형 DSL과 Variant API를 계속 사용할 수 있습니다. 하지만 이것은 어디까지나 임시방편입니다. AGP 10.0(2026년 중반 예정)에서는 이 옵션도 제거될 예정이니 지금부터 차근차근 마이그레이션을 준비하는 것이 좋아요.
배운 것들
AGP 9.0을 적용하는 건 단순히 빌드가 성공하게 만드는 작업이 아니었습니다. 안드로이드 빌드 시스템이 어떤 방향을 추구하고 있는지, 그리고 우리의 빌드 로직이 어떤 원칙 위에 서 있어야 하는지를 다시 생각하게 만든 계기였습니다.
CommonExtension은 만능 키가 아니다
CommonExtension에 의존하던 방식은 편리했지만 타입 안전성을 희생한 것이었어요. AGP 9.0의 변화는 이런 약점을 드러냈습니다.
Application과 Library는 다르다
두 모듈 타입은 근본적으로 다른 산출물을 만들고 다른 설정을 필요로 합니다. 이 차이를 인정하고 명시적으로 처리하는 것이 더 건강한 빌드 로직입니다.
빌드 로직도 코드다
코드를 작성할 때 타입 안전성, 가독성, 유지보수성을 고민하듯이 빌드 로직을 작성할 때도 같은 고민이 필요해요. 빌드 스크립트가 복잡해질수록 이런 원칙들의 중요성은 더욱 커집니다.
변화의 방향을 이해하자
AGP 7.x부터 새로운 DSL과 Variant API가 준비되기 시작했고, AGP 8.x에서 이 인터페이스들이 안정화되었으며, AGP 9.0에서 마침내 구형 API가 제거되었습니다. 도구가 변화하는 방향을 미리 파악하고 준비한다면 메이저 업데이트가 두려운 일이 아니라 자연스러운 과정이 될 수 있어요.
생태계와 함께 움직이기
개인 프로젝트라면 곧바로 AGP 9.0을 적용할 수 있겠지만 실제 프로덕션 환경에서는 서드파티 플러그인의 호환성도 고려해야 합니다. Firebase, Hilt, KSP 등 주요 플러그인들이 AGP 9.0을 지원하는지 확인하고, 필수가 아니라면 생태계가 안정될 때를 기다리는 것도 좋은 선택입니다.
마치며
AGP 9.0 마이그레이션을 고민하고 계신가요? 이 글이 AGP 9.0을 도입하려는 분들께 작은 도움이 되었기를 바랍니다. 여러분만의 빌드 로직 개선 경험도 공유해 주시면 좋겠습니다. 긴 글 읽어 주셔서 감사합니다.
'Android' 카테고리의 다른 글
| Google Mobile Ads Next-Gen SDK, 왜 만든건데? (0) | 2026.02.09 |
|---|---|
| BaseViewModel을 쓰지 않는 이유 (4) | 2026.01.30 |
| HttpLoggingInterceptor JSON 파싱 최적화로 95% 성능 개선하기 (0) | 2026.01.21 |
| Android CI 빌드 속도 1분대로 줄여보기 (0) | 2026.01.06 |
| OkHttp Authenticator에서 runBlocking 제거하기 (1) | 2025.11.27 |