AGP 9.0 마이그레이션, 뭐가 달라졌는데요?

2026. 1. 21. 19:26·Android

"이번엔 그냥 버전만 올리면 되겠지?(제발)"

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가 없습니다. 이런 차이를 제네릭으로 추상화하다 보면 컴파일 타임에 잡아낼 수 있는 오류가 런타임으로 미뤄질 위험이 있습니다.

그리고 제네릭 타입 파라미터가 6개나 되는 인터페이스는 유지보수가 어렵습니다. AGP 8.3에서 타입 파라미터가 5개에서 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. 뭔가 맘에 안드는데..

좀더 간결하게 표현을 해보고 싶었습니다. 이전 방식처럼 분기처리 없이 표현을 해보고 싶다는 생각이었는데요.

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년 중반 예정)에서는 이 옵션도 제거될 예정이니 지금부터 차근차근 마이그레이션을 준비하는 것이 좋겠죠👍🏻

TIL.

AGP 9.0 를 적용하는건 단순히 빌드가 성공하게 만드는 작업이 아닙니다. 안드로이드 빌드 시스템이 어떤 방향을 추구하고 있는지 그리고 우리의 빌드 로직이 어떤 원칙 위에 서 있어야 하는지를 다시 생각하게 만든 계기였습니다.

  • CommonExtension은 만능 키가 아니다
  • 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' 카테고리의 다른 글

모든 로그를 JSON으로 파싱하면 안 될까???  (0) 2026.01.21
Android CI 빌드 속도 1분대로 줄여보기  (0) 2026.01.06
OkHttp Authenticator에서 runBlocking 없이 토큰 갱신하기  (0) 2025.11.27
Credential Manager로 로그인이 안돼요. 이유는 몰라요  (2) 2025.11.18
[Android] 앱을 배포해 봅시다.  (0) 2025.10.02
'Android' 카테고리의 다른 글
  • 모든 로그를 JSON으로 파싱하면 안 될까???
  • Android CI 빌드 속도 1분대로 줄여보기
  • OkHttp Authenticator에서 runBlocking 없이 토큰 갱신하기
  • Credential Manager로 로그인이 안돼요. 이유는 몰라요
한민돌
한민돌
Android 개발자가 되기까지.
  • 한민돌
    미래 가젯 연구소
    한민돌
  • 전체
    오늘
    어제
    • 분류 전체보기 (27) N
      • Android (22) N
        • Compose (11)
        • Jetpack (2)
      • Kotlin (2)
      • 우아한테크코스 (3)
  • 블로그 메뉴

    • 홈
    • 안드로이드
    • 태그
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    compose
    우테코
    Android
    jetpack
    coroutine
    runcatching
    DisposableEffect
    Gradle
    build-logic
    LaunchedEffect
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
한민돌
AGP 9.0 마이그레이션, 뭐가 달라졌는데요?
상단으로

티스토리툴바