안드로이드 개발 생태계는 끊임없이 진화하고 있습니다. 이러한 변화의 중심에는 한때 '서포트 라이브러리(Support Library)'라 불렸던 구성 요소의 대대적인 개편, 즉 AndroidX로의 전환이 있습니다. 많은 개발자들이 "언젠가는 해야지"라고 생각하며 미뤄왔던 이 마이그레이션 작업은 이제 더 이상 선택이 아닌 필수가 되었습니다. 최신 라이브러리들은 AndroidX를 기본으로 출시되고 있으며, 구글 또한 AndroidX 사용을 강력히 권장하고 있습니다. 하지만 마이그레이션 과정은 결코 순탄하지만은 않습니다. 수많은 의존성 충돌, 예기치 않은 런타임 에러, 그리고 원인을 알 수 없는 빌드 실패는 개발자들을 좌절의 늪에 빠뜨리곤 합니다.
이 글은 바로 그 좌절의 지점에서 시작합니다. 단순히 "이 코드를 추가하세요"라는 단편적인 해결책을 넘어, 왜 AndroidX로 전환해야 하는지, 마이그레이션의 핵심 도구인 Jetifier는 어떤 원리로 동작하는지, 그리고 자동 마이그레이션 도구의 한계를 넘어 어떻게 프로젝트를 안정적으로 전환할 수 있는지에 대한 깊이 있는 통찰과 실전 노하우를 공유하고자 합니다. 이 가이드를 끝까지 따라오신다면, 당신은 더 이상 AndroidX 관련 오류를 두려워하지 않고, 오히려 이를 발판 삼아 더 견고하고 현대적인 앱을 구축할 수 있는 자신감을 얻게 될 것입니다.
1. 과거와의 작별: 왜 우리는 서포트 라이브러리를 떠나 AndroidX로 가야 하는가?
모든 변화에는 이유가 있습니다. AndroidX로의 전환 역시 과거 서포트 라이브러리가 가졌던 근본적인 문제점들을 해결하기 위한 필연적인 과정이었습니다.
1.1. 서포트 라이브러리의 시대: 혼돈의 서막
서포트 라이브러리의 초기 목표는 명확했습니다. 안드로이드의 파편화 문제를 해결하고, 하위 버전 OS에서도 최신 API와 기능을 사용할 수 있도록 지원하는 것이었죠. Fragment
, RecyclerView
, AppCompatActivity
등 오늘날 안드로이드 개발의 근간을 이루는 많은 클래스들이 바로 이 서포트 라이브러리를 통해 제공되었습니다.
하지만 시간이 흐르며 프로젝트는 점점 복잡해졌고, 서포트 라이브러리는 여러 문제점을 드러내기 시작했습니다.
- 버전 관리의 지옥: 모든 서포트 라이브러리 모듈(
support-v4
,appcompat-v7
,design
등)은 컴파일 SDK 버전(예: 27, 28)에 강하게 종속되었습니다. 이는 특정 라이브러리 하나만 업데이트하고 싶어도, 프로젝트의 모든 서포트 라이브러리 버전을 한꺼번에 맞춰 올려야 함을 의미했습니다. 이 과정에서 의존성 지옥(Dependency Hell)이 펼쳐지는 것은 흔한 일이었습니다. - 일관성 없는 패키지명: 어떤 클래스는
android.support.v4.app
에 있고, 다른 클래스는android.support.v7.widget
에 있는 등 패키지 구조가 직관적이지 않았습니다. 라이브러리가 추가될 때마다 새로운 명명 규칙이 생겨나 개발자들에게 혼란을 주었습니다. - 방대한 크기: 특히
support-v4
라이브러리는 너무 많은 기능을 담고 있어 '만물상자'처럼 비대해졌습니다. 단 하나의 기능만 필요하더라도 거대한 라이브러리 전체를 포함해야 하는 비효율이 발생했습니다.
1.2. AndroidX의 등장: 새로운 질서의 확립
AndroidX는 이러한 문제들을 해결하기 위해 완전히 새롭게 설계된 라이브러리 모음입니다. Jetpack의 핵심 구성 요소로서 다음과 같은 명확한 장점을 제공합니다.
- 의미론적 버전 관리(Semantic Versioning)와 독립성: 각 AndroidX 라이브러리는 이제 독립적으로 버전을 관리합니다.
androidx.core:core-ktx:1.9.0
과androidx.appcompat:appcompat:1.6.1
처럼, 각 라이브러리는 서로 다른 버전을 가질 수 있습니다. 이는 개발자가 필요한 라이브러리만 안정적으로 업데이트할 수 있게 해주며, 버전 충돌의 가능성을 대폭 줄여줍니다. - 일관되고 명확한 패키지 구조: 모든 라이브러리는
androidx.
라는 접두사로 시작합니다. 예를 들어,core
,appcompat
,recyclerview
등 기능에 따라 패키지명이 직관적으로 구성되어 있어 원하는 클래스를 찾기 훨씬 수월해졌습니다. - 정기적인 릴리즈와 발전: AndroidX는 운영체제 버전과 독립적으로 더 자주, 그리고 정기적으로 업데이트됩니다. 이를 통해 개발자들은 최신 버그 수정과 새로운 기능을 빠르게 적용할 수 있습니다.
- 필수불가결한 존재: Compose, Hilt, Room, ViewModel 등 현대적인 안드로이드 개발을 위한 Jetpack의 핵심 라이브러리들은 모두 AndroidX를 기반으로 합니다. 즉, 최신 기술 스택을 도입하기 위해서는 AndroidX로의 전환이 반드시 선행되어야 합니다.
결론적으로, AndroidX로의 마이그레이션은 단순히 낡은 것을 새것으로 바꾸는 작업이 아닙니다. 이는 혼란스러웠던 과거의 개발 방식과 작별하고, 더 안정적이고 효율적이며 미래 지향적인 개발 생태계로 진입하는 첫걸음입니다.
2. 마이그레이션의 핵심 열쇠: `useAndroidX`와 `Jetifier` 완전 분석
AndroidX 마이그레이션을 이야기할 때 절대 빼놓을 수 없는 두 가지 설정이 있습니다. 바로 `android.useAndroidX=true`와 `android.enableJetifier=true`입니다. 이 두 줄의 코드는 gradle.properties
파일에 추가되며, 마이그레이션 과정의 거의 모든 마법을 담당합니다. 하지만 이들이 정확히 어떤 역할을 하는지 이해하지 못하면, 문제가 발생했을 때 속수무책으로 당할 수밖에 없습니다. 이제 이 두 가지 플래그의 정체를 낱낱이 파헤쳐 보겠습니다.

프로젝트 루트의 gradle.properties 파일에 이 두 줄을 추가하는 것이 마이그레이션의 시작입니다.
2.1. `android.useAndroidX=true`: 전환의 스위치를 켜다
이 플래그는 이름 그대로 "우리 프로젝트는 이제 AndroidX를 사용하겠습니다"라고 안드로이드 그래들 플러그인(Android Gradle Plugin, AGP)에 선언하는 역할을 합니다. 이 선언이 활성화되면, AGP는 빌드 과정에서 다음과 같은 중요한 작업을 수행합니다.
- 의존성 재매핑(Dependency Remapping): AGP는 프로젝트의 `build.gradle` 파일에 명시된 구형 서포트 라이브러리 의존성을 해당하는 AndroidX 라이브러리로 자동으로 교체하여 인식합니다. 예를 들어, 개발자가 `implementation 'com.android.support:appcompat-v7:28.0.0'`이라고 코드를 그대로 두더라도, AGP는 빌드 시 이를 `androidx.appcompat:appcompat:1.0.0`에 해당하는 버전으로 취급합니다.
- 컴파일 타임 검증: 이 플래그는 플러그인이 소스 코드와 의존성을 분석하여 AndroidX 규칙에 맞는지 확인하는 기준점이 됩니다.
즉, useAndroidX=true
는 마이그레이션의 '의지'를 표명하는 가장 기본적인 스위치입니다. 하지만 이것만으로는 충분하지 않습니다. 우리 프로젝트만 AndroidX를 사용한다고 해서 모든 문제가 해결되지는 않기 때문입니다. 우리 프로젝트가 의존하는 수많은 서드파티 라이브러리들이 아직 서포트 라이브러리를 사용하고 있을 수 있습니다.
2.2. `android.enableJetifier=true`: 과거와 현재를 잇는 번역가
Jetifier는 AndroidX 마이그레이션의 진정한 영웅이자, 가장 많은 오해를 받는 도구이기도 합니다. 그의 역할은 바로 '번역가'입니다.
상황: 내 프로젝트(`My-App`)는 `android.useAndroidX=true`로 설정하여 AndroidX를 사용합니다. 하지만 내가 사용하는 외부 라이브러리(`Old-Library`)는 아직 업데이트되지 않아 내부적으로 구형 서포트 라이브러리(예: `android.support.v4.app.Fragment`)를 참조하고 있습니다.
문제: 이 상태로 빌드를 시도하면, `My-App`의 코드베이스에는 `androidx.fragment.app.Fragment`가 있고, `Old-Library`는 `android.support.v4.app.Fragment`를 참조하게 됩니다. 이 두 클래스는 사실상 같은 역할을 하지만 이름이 다르기 때문에 호환되지 않습니다. 더 심각한 경우, 다른 라이브러리를 통해 두 버전의 클래스가 모두 포함되면서 'Duplicate class' 오류를 내뿜으며 빌드가 실패하게 됩니다.
해결사 Jetifier의 등장: `android.enableJetifier=true` 플래그가 활성화되면, AGP는 빌드 과정에서 Jetifier를 실행시킵니다. Jetifier는 다음과 같은 놀라운 작업을 수행합니다.
- 의존성 스캔: 프로젝트에 포함된 모든 라이브러리(AAR, JAR 파일)를 검사합니다. 이는 직접 추가한 라이브러리뿐만 아니라, 그 라이브러리가 의존하는 다른 라이브러리들(transitive dependencies)까지 모두 포함합니다.
- 바이트코드 분석 및 수정: 스캔한 라이브러리들의 바이트코드를 직접 읽어들입니다. 그리고 `android.support.**` 패키지를 참조하는 모든 코드를 찾아내어, 이에 상응하는 `androidx.**` 패키지 참조로 즉석에서 수정(rewrite)합니다.
- 수정된 라이브러리 사용: 이렇게 실시간으로 '번역'된 라이브러리를 사용하여 최종 앱 패키지(APK)를 빌드합니다.
결과적으로, `Old-Library`는 원래 서포트 라이브러리를 참조하도록 만들어졌지만, Jetifier 덕분에 우리 프로젝트 내에서는 마치 처음부터 AndroidX를 사용하도록 만들어진 것처럼 동작하게 됩니다. 이 덕분에 개발자는 서드파티 라이브러리 개발사가 AndroidX로 업데이트해주기를 마냥 기다리지 않고도, 자신의 프로젝트를 먼저 마이그레이션할 수 있는 유연성을 얻게 됩니다.
하지만 이 강력한 기능에는 비용이 따릅니다. 모든 의존성을 스캔하고 바이트코드를 수정하는 작업은 빌드 시간에 상당한 오버헤드를 추가합니다. 프로젝트의 규모가 크고 의존성이 많을수록 빌드 속도가 눈에 띄게 느려지는 원인이 바로 Jetifier입니다. 따라서 Jetifier는 마이그레이션 '과정'에서 필수적인 도구이지만, 장기적으로는 모든 라이브러리를 AndroidX 네이티브 버전으로 업데이트하여 Jetifier를 비활성화하는 것이 이상적인 목표가 됩니다.
3. 실전! 단계별 AndroidX 마이그레이션 가이드
이론을 알았으니 이제 실전으로 나아갈 차례입니다. 안드로이드 스튜디오가 제공하는 자동화 도구는 매우 강력하지만, 100% 완벽하지는 않습니다. 성공적인 마이그레이션을 위해서는 자동화와 수동 검증의 조화가 필수적입니다.
1단계: 사전 준비 (가장 중요!)
전쟁터에 나가기 전 군장을 점검하듯, 마이그레이션 전 준비는 아무리 강조해도 지나치지 않습니다.
- 프로젝트 백업: 반드시, 반드시 Git과 같은 버전 관리 시스템을 사용해 현재 안정적인 상태의 프로젝트를 커밋(commit)하세요. 문제가 생겼을 때 돌아갈 수 있는 안전장치가 있다는 사실만으로도 심리적 안정감을 줍니다.
- 안드로이드 스튜디오 업데이트: 최신 버전의 안드로이드 스튜디오는 더 향상된 마이그레이션 도구와 버그 수정을 포함하고 있습니다.
Help > Check for Updates
를 통해 최신 버전으로 업데이트하세요. - 의존성 버전 확인: 마이그레이션 전에 주요 라이브러리들(Dagger, Retrofit, OkHttp 등)의 최신 버전을 확인하고, 가능하면 미리 업데이트해두는 것이 좋습니다. 최신 버전일수록 AndroidX를 공식 지원할 확률이 높습니다.
2단계: 안드로이드 스튜디오 마법사 실행
준비가 완료되었다면, 안드로이드 스튜디오의 마이그레이션 도구를 사용할 차례입니다.
- 메뉴 바에서 Refactor > Migrate to AndroidX... 를 선택합니다.
- 백업을 권장하는 대화상자가 나타납니다. 우리는 이미 1단계에서 백업을 완료했습니다. 체크박스를 선택하고 [Migrate] 버튼을 클릭합니다.
- 안드로이드 스튜디오가 프로젝트 전체를 분석하기 시작합니다. 분석이 끝나면 리팩토링이 필요한 코드 목록을 보여주는 'Refactoring Preview' 창이 나타납니다.
- 하단의 [Do Refactor] 버튼을 클릭하면 실제 마이그레이션 작업이 진행됩니다.
이 마법사가 수행하는 작업은 다음과 같습니다.
- `gradle.properties` 파일에 `android.useAndroidX=true`와 `android.enableJetifier=true`를 추가합니다.
- 모든 모듈의 `build.gradle` 파일을 스캔하여 `com.android.support.*` 의존성을 `androidx.*`로 변경합니다.
- Java 및 Kotlin 소스 코드의 `import` 구문을 `androidx.*`로 변경합니다.
- XML 레이아웃 파일에 하드코딩된 위젯 경로(예: `android.support.v7.widget.RecyclerView`)를 `androidx.recyclerview.widget.RecyclerView`로 변경합니다.
3. 수동 검증 및 후처리
자동 마이그레이션이 끝난 후, "이제 끝났다!"라고 생각하면 오산입니다. 지금부터가 진짜 실력이 드러나는 구간입니다.
- 전체 클린 및 리빌드: 가장 먼저 할 일입니다. 안드로이드 스튜디오 메뉴에서 Build > Clean Project를 실행한 후, Build > Rebuild Project를 실행합니다. 이 과정에서 자동 도구가 놓친 문제점들이 빌드 오류로 나타나기 시작합니다.
-
`build.gradle` 파일 수동 검토:
프로젝트의 모든 `build.gradle(.kts)` 파일을 열어보세요. 특히 `dependencies` 블록을 집중적으로 확인합니다.
- 아직 `com.android.support`로 남아있는 의존성이 있는지 확인하고, 있다면 직접 수정합니다.
- 주석 처리된 코드나 빌드 스크립트 내의 문자열에 포함된 옛날 라이브러리 이름도 확인해야 합니다.
- 매핑 규칙 확인: 대부분의 라이브러리는 예측 가능한 규칙으로 매핑되지만, 일부는 다릅니다. 예를 들어, `com.android.support:design`은 `com.google.android.material:material`로 변경됩니다. 공식 클래스 매핑 문서와 아티팩트 매핑 문서를 참고하여 올바르게 변경되었는지 확인하세요.
[Before]
dependencies { implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support:recyclerview-v7:28.0.0' implementation 'com.android.support:design:28.0.0' // Annotation processor kapt 'com.google.dagger:dagger-compiler:2.16' }
[After - Ideal]
dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' // 최신 안정화 버전으로 implementation 'androidx.recyclerview:recyclerview:1.3.0' implementation 'com.google.android.material:material:1.9.0' // Annotation processor도 AndroidX 호환 버전으로 업데이트 kapt 'com.google.dagger:dagger-compiler:2.44' }
-
ProGuard/R8 규칙 업데이트: `proguard-rules.pro` 파일에 서포트 라이브러리 클래스 경로를 직접 명시했다면, 이 역시 `androidx` 경로로 수정해야 합니다. 그렇지 않으면 릴리즈 빌드에서 클래스를 찾지 못해 앱이 크래시될 수 있습니다.
[Before]
-keep class android.support.v7.widget.SearchView { *; }
[After]
-keep class androidx.appcompat.widget.SearchView { *; }
- 전체 코드 검색: 안드로이드 스튜디오의 `Find in Files` (Ctrl+Shift+F 또는 Cmd+Shift+F) 기능을 사용하여 프로젝트 전체에서 "android.support" 문자열을 검색하세요. 빌드 스크립트, 주석, 문자열 리터럴 등 예상치 못한 곳에 남아있는 흔적을 찾아 제거해야 합니다.
- 빌드, 실행, 그리고 철저한 테스트: 모든 수정이 완료되었다면 다시 클린 & 리빌드를 수행하고, 앱을 실행하여 모든 기능이 정상적으로 동작하는지 꼼꼼히 테스트합니다. 특정 화면이 깨지거나, 특정 버튼이 동작하지 않는 등의 문제가 발생할 수 있습니다.
4. 마이그레이션 지뢰밭: 흔히 겪는 함정과 해결 전략
마이그레이션 과정은 지뢰밭을 걷는 것과 같습니다. 다음은 개발자들이 가장 흔하게 밟는 '지뢰'들과 그 해체 방법입니다.
유형 1: "Duplicate class ... found in modules" 오류
- 원인: 가장 흔한 오류입니다. 이는 프로젝트의 의존성 그래프 어딘가에 동일한 클래스를 가진 서포트 라이브러리와 AndroidX 라이브러리가 동시에 포함되었음을 의미합니다. 보통 Jetifier가 해결해주지만, 특정 라이브러리 설정이나 복잡한 의존성 구조 때문에 실패하는 경우가 있습니다.
- 해결 전략:
- Jetifier 활성화 확인: `gradle.properties`에 `android.enableJetifier=true`가 확실히 있는지 다시 한번 확인하세요.
- 의존성 트리 분석: 터미널에서 `./gradlew app:dependencies` 명령을 실행하여 프로젝트의 전체 의존성 트리를 출력합니다. 오류 메시지에 언급된 중복 클래스를 어떤 라이브러리가 포함하고 있는지 추적합니다. 보통 `com.android.support`를 끌고 오는 '주범' 라이브러리를 찾을 수 있습니다.
- 수동으로 의존성 제외(exclude): 주범을 찾았다면, `build.gradle`에서 해당 라이브러리를 추가할 때 서포트 라이브러리 그룹을 명시적으로 제외할 수 있습니다.
implementation('some.old.library:1.0.0') { exclude group: 'com.android.support' // 또는 특정 모듈만 제외할 수도 있습니다. // exclude module: 'support-v4' }
유형 2: 빌드 시간의 급격한 증가
- 원인: 앞서 설명했듯, 범인은 Jetifier입니다. 의존성이 많을수록 그의 번역 작업은 길어집니다.
- 해결 전략:
- 단기적 해결책: 받아들이세요. 마이그레이션 과정에서는 안정성이 속도보다 중요합니다.
- 장기적 해결책: '탈-Jetifier'를 목표로 삼아야 합니다. 주기적으로 프로젝트의 의존성을 감사(audit)하고, 서포트 라이브러리를 사용하는 낡은 라이브러리들을 AndroidX 네이티브 버전으로 꾸준히 업데이트해주세요. 모든 라이브러리가 AndroidX 네이티브가 되면, `gradle.properties`에서 `android.enableJetifier=false`로 설정하여 눈에 띄게 빨라진 빌드 속도를 경험할 수 있습니다.
유형 3: 특정 라이브러리와의 충돌 (특히 Annotation Processor)
- 원인: Dagger, Hilt, Realm, Epoxy 등 어노테이션 프로세서를 사용하는 라이브러리들은 코드 생성 단계에서 클래스 경로에 매우 민감합니다. 라이브러리 자체는 AndroidX로 마이그레이션되었더라도, `kapt`나 `annotationProcessor`로 추가된 컴파일러/프로세서 버전이 낡아서 구형 서포트 라이브러리 코드를 생성하려 할 때 문제가 발생합니다.
- 해결 전략:
- 라이브러리 쌍(Pair) 업데이트: 해당 라이브러리의 구현부(implementation)와 어노테이션 프로세서(kapt/annotationProcessor)의 버전을 반드시 함께, 그리고 AndroidX를 지원하는 최신 버전으로 업데이트해야 합니다. 라이브러리 공식 문서를 확인하여 AndroidX 지원 버전을 명확히 확인하는 것이 중요합니다.
유형 4: 런타임 크래시 (ClassNotFoundException, MethodNotFoundException 등)
- 원인: 빌드는 성공했지만 앱 실행 중 특정 클래스나 메소드를 찾을 수 없다는 오류가 발생합니다. 이는 보통 ProGuard/R8 규칙이 업데이트되지 않았거나, 리플렉션을 사용하는 코드에서 옛 클래스 경로를 문자열로 하드코딩한 경우에 발생합니다.
- 해결 전략:
- `proguard-rules.pro` 파일을 가장 먼저 확인하고, 모든 `android.support` 경로를 `androidx`로 수정합니다.
- 프로젝트 전체에서 `Class.forName("android.support...")`와 같이 리플렉션을 사용하는 코드가 있는지 검색하고 수정합니다.
- XML 레이아웃 파일에 커스텀 뷰의 경로가 문자열로 포함된 경우도 확인해야 합니다.
결론: 마이그레이션은 끝이 아닌 새로운 시작
AndroidX로의 마이그레이션은 단순히 의존성 몇 줄을 바꾸는 기계적인 작업이 아닙니다. 그것은 과거의 기술 부채를 청산하고, 안드로이드 개발의 현대적인 패러다임을 온전히 수용하기 위한 필수적인 통과 의례입니다. `useAndroidX`와 `Jetifier`라는 강력한 도구가 우리의 여정을 돕지만, 결국 성공적인 마이그레이션의 완성은 개발자의 세심한 검토와 문제 해결 능력에 달려 있습니다.
이 가이드에서 제시한 단계별 전략과 문제 해결법을 통해, 여러분이 겪을 수 있는 시행착오를 최소화할 수 있기를 바랍니다. 기억하세요. AndroidX로의 성공적인 전환은 단지 오류를 해결하는 것에서 그치지 않습니다. 이는 Jetpack Compose의 선언형 UI, Hilt를 통한 손쉬운 의존성 주입, Room을 이용한 강력한 데이터베이스 관리 등, 안드로이드 개발의 미래를 향한 문을 활짝 여는 열쇠입니다. 이제 두려움을 떨치고, 더 견고하고 발전된 앱을 만들기 위한 여정을 시작할 때입니다.
0 개의 댓글:
Post a Comment