Android ProGuard 난독화에서 모바일 SDK를 제외하려면 어떻게 해야 할까요? Android SDK를 난독화에서 제외하려면 해당 라이브러리에 소비자 ProGuard 규칙이 포함되어 있는지 확인하고, 리플렉션을 통해 액세스되는 진입점, 콜백 인터페이스, JNI 브리지 및 데이터 모델에 대해 좁게 범위를 지정한 -keep 및 -keepclassmembers 지시문을 추가한 뒤, R8 진단 보고서와 원시 DEX 검사를 통해 보존된 심볼을 감사해야 합니다. Android 엔지니어링 팀이 프로덕션 릴리스 빌드를 위해 코드 축소 및 최적화를 활성화하면, R8 컴파일러는 참조되지 않은 바이트코드를 제거하고 클래스, 필드 및 메서드 이름을 짧은 식별자로 변경합니다. 만약 모바일 어트리뷰션 소프트웨어 개발 키트(SDK)가 공격적인 트리 쉐이킹(tree-shaking) 및 이름 변경으로부터 보호되지 않는 동적 리플렉션이나 JNI 콜백에 의존하는 경우, 미니파이되지 않은 디버그 빌드에서 모든 테스트를 통과하더라도 프로덕션 환경에서 지연된 딥링크(deferred deep linking), JSON 모델 역직렬화, 채널 패키지 빌드가 조용히 실패할 수 있습니다. 모바일 어트리뷰션 및 딥링크 플랫폼인 OpoInstall은 엔지니어링 팀이 R8로 축소된 릴리스 파이프라인 내에서 구성하고 검증할 수 있는 네이티브 Android SDK 아카이브와 채널 패키지 워크플로우를 제공합니다.
현대적인 Android Gradle Plugin(AGP) 툴체인에서 유지 규칙(keep-rule) 파일(기존 DSL의
proguard-rules.pro또는 AGP 9.3+의src/<variant>/keepRules/*.keep)은 Google R8 컴파일러의 규칙 구성 계약 역할을 합니다. 타사 Android SDK를 난독화에서 안전하게 제외한다는 것은 런타임에 필요한 특정 공개 API 진입점, 리플렉티브 생성자, 콜백 인터페이스, JNI 업콜 및 모델 필드만 보존하고, 패키지 전체에 대한 광범위한 유지 규칙이나 검증되지 않은 경고 억제를 피하는 것을 의미합니다.
| 용어 | 정의 | 관련 엔티티 | 검색 의도 역할 |
|---|---|---|---|
| Android SDK | 설치 어트리뷰션과 같은 전문 기능을 제공하기 위해 Android 앱에 통합된 컴파일된 Java 또는 Kotlin 라이브러리. | classes.dex 바이트코드 |
탐색형 / 거래형 |
| ProGuard / R8 | 사용되지 않는 코드를 축소하고 바이트코드를 최적화하며 ProGuard 규칙 구문을 사용하여 식별자를 난독화하는 Android 빌드 타임 도구. | proguard-rules.pro |
기술적 / 정보형 |
| 소비자 ProGuard 규칙 | .aar(/proguard.txt) 또는 .jar(/META-INF/proguard/) 내부에 번들로 제공되어 R8로 자동 병합되는 난독화 규칙. |
consumerProguardFiles |
기술적 / 문제 해결 |
| R8 진단 보고서 | 심볼 이름 변경, 일치하는 유지 시드, 제거된 코드 등을 자세히 설명하는 출력 파일(mapping.txt, seeds.txt, usage.txt, configuration.txt). |
릴리스 빌드 감사 | 기술적 / 문제 해결 |
R8 및 ProGuard 난독화가 Android SDK 어트리뷰션 콜백을 중단시키는 이유

디버그와 릴리스의 함정: 테스트에서는 작동하지만 프로덕션에서 실패하는 이유
모바일 사용자 확보 과정에서 가장 흔한 릴리스 엔지니어링 회귀 중 하나는 앱의 지연된 딥링크(handleDeepLink) 및 설치 매개변수 복원(getInstallParam)이 디버그 빌드의 QA 테스트 중에는 안정적으로 작동하다가, 앱이 서명된 릴리스 APK 또는 Android App Bundle(.aab)로 컴파일되면 빈 페이로드를 반환하거나 콜백 실행을 멈추는 경우입니다.
근본 원인은 Gradle에 구성된 빌드 유형 차이에 있습니다. 기본적으로 debug 빌드 유형은 증분 빌드 속도를 높이기 위해 코드 축소를 비활성화합니다. 엔지니어가 R8 최적화가 활성화된 release 변형을 컴파일할 때, Android Gradle Plugin은 Android R8 컴파일러를 호출하여 호스트 앱과 연결된 타사 라이브러리 모두에 대해 세 가지 바이트코드 변환을 실행합니다:
- 코드 축소(Tree Shaking): R8은 선언된 진입점(예:
AndroidManifest.xml구성 요소 및 명시적-keep시드)에서 시작하여 애플리케이션의 호출 그래프를 분석하고 정적 분석을 통해 도달할 수 없는 것으로 보이는 클래스, 생성자, 메서드 또는 필드를 제거합니다. - 바이트코드 최적화 및 액세스 수정: R8은 메서드를 인라인화하고, 사용되지 않는 매개변수를 제거하며, 클래스를 병합합니다. 또한 Android 기본 최적화 구성에서
-allowaccessmodification지시문과 결합할 경우, Dalvik Executable(.dex) 크기를 줄이고 런타임 실행 속도를 개선하기 위해 (인라인화 중에private또는 package-private 멤버를public으로 확장하는 것과 같이) 클래스 및 멤버 가시성 플래그를 수정합니다. - 식별자 난독화(이름 변경 및 패키지 재구성): R8은 남아 있는 클래스, 패키지, 필드 및 메서드를
a.a.b()와 같은 압축된 식별자로 이름을 변경하며, 난독화된 클래스를 평탄화된 패키지 계층 구조로 재구성하여classes.dex내부의 문자열 풀 오버헤드를 줄일 수 있습니다.
R8 전체 모드(Full Mode)는 Android Gradle Plugin(AGP) 8.0부터 기본적으로 활성화되었습니다. 기존 ProGuard 호환성 모드와 비교하여 R8 전체 모드는 훨씬 더 엄격한 최적화를 적용합니다. 즉, 관련 클래스와 멤버가 일치하는 유지 규칙에 의해 명시적으로 유지되지 않는 한 기본 인수 없는 생성자(<init>())를 제거하고, 멤버 액세스 수정자를 변경하며, 메타데이터 속성을 제거합니다. .aar 바이너리 구조 및 SDK 조달 표준에 대한 기초적인 내용은 당사의 보완 가이드인 모바일 SDK의 의미와 기술적 성장 프로토콜 이해하기 및 정확한 어트리뷰션을 위한 소프트웨어 개발 키트 선택 방법을 참조하십시오.
난독화된 어트리뷰션 SDK에서의 6가지 고유한 실패 모드

정적 호출 그래프 분석으로는 동적 런타임 조회를 예측할 수 없기 때문에, 보호되지 않은 Android SDK는 R8 전체 모드에서 6가지 고유한 실패 모드를 경험할 수 있습니다. 엔지니어는 각 실패 모드마다 구체적인 ProGuard 보호 대상이 필요하므로 이를 신중하게 구분해야 합니다:
| 런타임 실패 모드 | 근본적인 R8 바이트코드 원인 | 필요한 특정 보호 대상 |
|---|---|---|
1. 리플렉티브 클래스 조회 실패 (ClassNotFoundException) |
Class.forName()을 통해 문자열 이름으로 동적으로 로드되는 클래스를 R8이 이름 변경, 패키지 재구성 또는 제거함 |
-keep class ...를 사용하여 대상 클래스와 원래 식별자를 보존 |
2. 리플렉티브 객체 인스턴스화 실패 (NoSuchMethodException: <init>) |
R8 전체 모드에서는 클래스가 유지되더라도 코드에서 직접 호출되지 않으면 기본 인수 없는 생성자(<init>())가 제거됨 |
클래스 -keep 블록 내부에 생성자 서명(<init>();)을 명시적으로 보존 |
| 3. 동적 콜백 및 액세스 수정 실패 | R8이 콜백 인터페이스 메서드를 제거하거나 이름을 변경하거나, 리플렉티브 getDeclaredMethod() / getDeclaredField() 호출에 필요한 멤버 가시성 플래그를 변경함 |
콜백 인터페이스 메서드(-keep interface ... { <methods>; }) 및 리플렉티브하게 액세스되는 모든 멤버를 보존 |
| 4. 콜백 리스너의 제네릭 서명 메타데이터 손실 | Java 제네릭은 컴파일 타임에 타입 소거를 거치지만, 바이트코드 Signature 속성은 런타임 리플렉션(ResultCallBack<OpoData>)을 위해 제네릭 메타데이터를 보존함. R8 전체 모드는 명시적으로 유지되지 않는 한 Signature 메타데이터를 제거함 |
제네릭 콜백 인터페이스와 대상 모델 클래스를 모두 유지하는 것 외에도 -keepattributes Signature를 유지 |
| 5. JSON 데이터 모델 역직렬화 실패 | 모델 클래스가 리플렉티브하게만 인스턴스화되거나 필드 이름이 a, b, c로 변경되어 리플렉션 기반 JSON 파서가 null 또는 빈 필드를 반환함 |
모델이 동적으로 인스턴스화되는 경우 -keep class ...를 사용하거나, 클래스가 직접 참조를 통해 이미 유지되는 경우 -keepclassmembers를 사용 |
6. JNI 다운콜 및 네이티브-to-Java 업콜 제거 (UnsatisfiedLinkError / NoSuchMethodError) |
기본 Android 규칙은 생존한 Java native 다운콜의 이름은 보존하지만, 사용되지 않는 RegisterNatives 메서드가 축소되는 것을 방지하지 못하며, JNI(FindClass, GetMethodID)를 통해 Java로 다시 호출하는 C/C++ .so 코드를 보지 못함 |
동적으로 등록된 native 선언과 C/C++에서 JNI 업콜을 통해 호출되는 모든 Java 클래스, 생성자 및 메서드를 명시적으로 -keep 처리 |
[Android 릴리스 빌드: R8 전체 모드 활성화]
|
v
[R8이 번들 및 호스트 유지 규칙을 평가]
|
+---------------+---------------+
| |
v (누락 / 불일치 규칙) v (범위가 지정된 유지 규칙 + 속성 활성화)
[SDK 심볼 제거 또는 이름 변경됨] [필수 SDK 진입점 및 모델 보존]
- 리플렉티브 () 제거 - -printseeds (seeds.txt)에서 확인됨
- JNI 업콜 / 모델 필드 손실 - 원시 DEX에서 검증됨 (apkanalyzer dex packages)
- 제네릭 서명 메타데이터 손실 - 디버그 동작과 일치하는 릴리스 어트리뷰션
AAR 번들 내의 소비자 규칙 vs. 수동 JAR 가져오기
AAR 및 JAR 라이브러리가 소비자 ProGuard 규칙을 번들링하는 방법

호스트 애플리케이션에 수동 제외 규칙을 추가하기 전에 타사 Android SDK가 이미 빌드된 R8 규칙을 번들로 제공하는지 확인하십시오. 라이브러리 최적화에 대한 Google Android 개발자 가이드에 문서화된 바와 같이, AGP와 R8은 버전별 R8 규칙을 포함하여 .aar 및 .jar 배포 전반에서 자동 소비자 규칙 발견을 지원합니다:
| SDK 배포 형식 | 포함된 소비자 규칙 위치 | Android Gradle Plugin / R8 빌드 동작 |
|---|---|---|
Android 아카이브 (.aar) |
AAR 루트의 /proguard.txt(consumerProguardFiles를 통해), 또는 /classes.jar 내부 /META-INF/com.android.tools/r8-from-<X>-upto-<Y>/ 하위의 버전별 규칙 |
컴파일 시 호스트 앱의 R8 구성으로 자동 추출 및 병합됨 |
규칙이 번들된 Java 아카이브 (.jar) |
/META-INF/proguard/*.pro 또는 /META-INF/com.android.tools/r8-from-<X>-upto-<Y>/ (r8, r8-from-<X>, r8-upto-<Y> 변형 포함) |
활성 R8 컴파일러 버전에 따라 AGP 및 R8에 의해 자동 발견 및 적용됨 |
규칙이 번들되지 않은 독립형 .jar 또는 .aar |
아카이브 내부에 존재하지 않음 | 번들된 소비자 보호가 제공되지 않음; 런타임 리플렉션, JNI, 직렬화 또는 기타 동적으로 액세스되는 심볼이 명시적 보존을 요구할 때만 호스트 수준의 유지 규칙을 추가 |
(참고: Gson 직렬화 규칙에 관하여 - ProGuard 및 R8에 대한 Gson 문제 해결 가이드에 문서화된 대로, Gson 2.11.0 이상은 TypeToken 및 @SerializedName으로 주석 처리된 필드에 대한 R8 규칙을 번들로 제공합니다. 이러한 규칙이 있다고 해서 리플렉티브 모델 액세스를 제한하거나, 호환되는 인수 없는 생성자를 제공하거나, 미니파이된 릴리스 빌드에서 모든 직렬화 및 역직렬화 경로를 테스트할 필요성이 사라지는 것은 아닙니다.)
규칙을 작성하기 전에 다운로드한 SDK 아카이브 검사
ProGuard 및 R8 규칙 일치는 엄격하게 대소문자를 구분하며 패키지 경로와 정확히 일치해야 합니다. SDK 버전이나 지역별 배포판마다 패키지 네임스페이스가 다를 수 있습니다. 만약 패키지 대문자 표기가 실제 아카이브의 .class 파일과 일치하지 않는 이전 튜토리얼의 규칙을 복사하면 R8은 조용히 0개의 클래스와 일치하게 됩니다.
유지 규칙을 편집하기 전에 대상 SDK 패키지(예: 모바일 SDK 다운로드 센터의 Android SDK v2.1.0)를 다운로드하고, 정확한 .class 계층 구조와 함께 내장된 규칙(/proguard.txt 및 classes.jar 내부)을 검사하십시오:
# 1. .aar 아카이브에서 루트 소비자 규칙(proguard.txt)을 검사하고 classes.jar를 추출하여 META-INF 규칙을 확인합니다
unzip -l opoinstall-android-sdk.aar
unzip -p opoinstall-android-sdk.aar proguard.txt
unzip -p opoinstall-android-sdk.aar classes.jar > temp_classes.jar
jar tf temp_classes.jar | grep -E "META-INF/proguard/|META-INF/com\.android\.tools/|\.class$"
# 2. 독립형 .jar 아카이브를 사용하는 경우, 포함된 META-INF 규칙과 정확한 대소문자 구분 .class 경로를 검사합니다
jar tf libs/OpoInstall_SDK.jar | grep -E "META-INF/proguard/|META-INF/com\.android\.tools/|\.class$"
어트리뷰션 Android SDK를 위한 proguard-rules.pro 구성
패키지 전체 유지 안티패턴 및 무분별한 경고 억제 방지
사용자 지정 규칙을 작성할 때 엔지니어는 불필요한 최적화 비활성화를 피하기 위해 Google의 유지할 항목 선택 (R8 유지 규칙) 모범 사례를 따라야 합니다:
- 패키지 전체
-keep class package.** { *; }안티패턴보다 좁은 범위의 규칙 선호: 패키지 전체에 대한 광범위한 규칙은 R8이 해당 패키지의 모든 내부 헬퍼 클래스를 유지하고 최적화를 건너뛰도록 강제합니다. 규칙의 범위를 런타임에 동적으로 액세스되는 특정 클래스, 생성자(<init>();), 콜백 인터페이스, JNI 대상 및 모델 필드로 지정하십시오(또는 제어하는 코드에 Jetpack의@UnconditionallyKeep주석을 사용하십시오). - 데이터 모델에서
-keep과-keepclassmembers구분:-keepclassmembers는 포함된 클래스 자체가 호출 그래프에 유지되는 경우에만 지정된 필드나 메서드를 보존합니다. 데이터 모델(예:OpoData)이 리플렉션을 통해서만 인스턴스화되거나 소거된 제네릭 타입을 통해서만 참조되는 경우, R8이 클래스 자체를 제거하지 않도록-keep class ...를 사용하십시오. - JNI에 대한
-keepclasseswithmembernames제한 사항 이해: Android의 기본 최적화 규칙에는-keepclasseswithmembernames,includedescriptorclasses class * { native <methods>; }가 포함되어 있습니다. Google의 R8 유지 규칙 문서 및 ProGuard 유지 지시문 매뉴얼에 정의된 대로,-keepclasseswithmembernames는-keepclasseswithmembers,allowshrinking과 동일합니다. 이는 축소 단계를 통과한 일치하는 클래스와 멤버에 대해 이름을 보존하지만, 사용되지 않는 클래스나native메서드가 축소 중에 제거되는 것을 방지하지는 않습니다. 직접적인 Java 호출 사이트 없이RegisterNatives를 통해 네이티브 메서드가 동적으로 등록되거나, C/C++ 코드가 JNI 업콜을 통해 Java 메서드를 호출하는 경우, 대상 클래스가 인스턴스화되는 방식에 따라 명시적인-keep또는-keepclassmembers규칙을 사용해야 합니다. - 검증되지 않은 무분별한
-dontwarn지시문 피하기:-dontwarn은 축소나 이름 변경에 대한 보호를 전혀 제공하지 않으며, 런타임NoClassDefFoundError충돌을 숨길 수 있는 미해결 참조 경고를 억제합니다. 빌드 로그가 런타임에 선택 사항임을 확인한 특정 컴파일 전용 클래스가 없다고 보고하는 경우에만 좁은 범위의-dontwarn을 추가하십시오.
| ProGuard / R8 지시문 | 등가 긴 형식 구문 | 축소 (제거) | 난독화 (이름 변경) | 권장 엔지니어링 범위 |
|---|---|---|---|---|
-keep class ... |
-keep |
방지됨 (클래스 및 지정된 멤버 유지) | 방지됨 (원래 이름 유지) | 공개 SDK 진입점, 콜백 인터페이스, 리플렉션이나 JNI를 통해 동적으로 인스턴스화된 클래스(<init>(...); 포함) |
-keepclassmembers class ... |
-keepclassmembers |
클래스가 유지되는 경우에만 멤버 보존 | 일치하는 멤버에 대해 방지됨 | Java/Kotlin에서 이미 직접 인스턴스화된 클래스의 <fields>; 또는 JNI 업콜 메서드 보존 |
-keepclasseswithmembernames |
-keepclasseswithmembers,allowshrinking |
참조되지 않는 경우 허용 (allowshrinking) |
클래스 및 멤버가 축소 단계에서 살아남으면 방지됨 | Java 코드가 native 메서드를 직접 호출할 때의 기본 JNI 다운콜 이름 보존(native <methods>;) |
-keepnames class ... |
-keep,allowshrinking |
참조되지 않는 경우 허용 (allowshrinking) |
클래스가 유지되면 방지됨 | 사용되지 않는 클래스를 classes.dex에 강제로 포함하지 않으면서 스택 추적을 위한 읽기 가능한 클래스 이름 유지 |
-keepattributes ... |
-keepattributes |
보존된 항목의 메타데이터 보존 | N/A (바이트코드 속성) | 명시적으로 보존된 클래스에 대해 Signature(제네릭), InnerClasses, EnclosingMethod 및 *Annotation* 유지 |
-dontwarn <specific_class> |
-dontwarn |
축소에 영향 없음 | 이름 변경에 영향 없음 | 검증된 선택적 컴파일 전용 종속성에 대해서만 컴파일 타임 경고 억제 |
AGP 8.x 및 AGP 9.3+ 툴체인 전반의 Gradle 구성
Android Gradle Plugin 9.3은 간소화된 optimization DSL 블록과 변형별 keepRules 디렉토리(.keep 파일)를 도입한 반면, AGP 8.x부터 9.2(및 AGP 9.3에서도 지원되는 레거시 구성)는 isMinifyEnabled와 proguardFiles를 사용합니다. 엔지니어는 세 가지 구성 객체를 명확히 구분해야 합니다:
| 구성 객체 | 파일 또는 DSL 위치 | R8 빌드에서의 아키텍처 역할 |
|---|---|---|
| 레거시 DSL 유지 규칙 (AGP 8.x–9.2 & 레거시 9.3+) | app/proguard-rules.pro (proguardFiles(...)를 통해 참조됨) |
getDefaultProguardFile("proguard-android-optimize.txt")와 함께 호스트 유지 규칙 선언 |
| 새로운 최적화 DSL 유지 규칙 (AGP 9.3+) | app/src/main/keepRules/*.keep 또는 app/src/<variant>/keepRules/*.keep |
optimization { enable = true }를 사용할 때 .keep 파일 선언 (기본 Android 플랫폼 규칙은 자동으로 적용됨; 레거시 DSL도 .keep 소스 세트를 지원함) |
| R8 진단 보고서 지시문 | 검증된 R8 구성 파일(proguard-rules.pro)의 -printseeds, -printusage, -printconfiguration |
R8이 감사 보고서를 app/build/outputs/mapping/<buildVariant>/로 내보내도록 지시 |
// 옵션 A: AGP 8.x - 9.2 레거시 DSL (AGP 9.3+에서도 지원) — app/build.gradle.kts
android {
buildTypes {
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
// 옵션 B: AGP 9.3+ 간소화된 최적화 DSL — app/build.gradle.kts
// 앱/src/main/keepRules/sdk-attribution.keep에 범위가 지정된 클래스/멤버 유지 규칙 배치
android {
buildTypes {
release {
optimization {
enable = true
}
}
}
}
예시용 범위 지정 유지 규칙 및 진단 지시문
OpoInstall의 공개 문서와 과거 FAQ 페이지는 서로 다른 패키지 접두사(com.opoinstall.api.** 대 com.fm.OpoInstall.** 또는 io.OpoInstall.**)를 참조하므로, 아래 규칙 세트는 예시일 뿐이며 OpoInstall SDK v2.1.0을 위한 검증된 프로덕션 규칙 세트가 아닙니다. 다운로드한 v2.1.0 아카이브 내부의 실제 클래스 경로와 번들로 제공된 소비자 규칙을 먼저 검사하고, OpoInstall 기술 문서를 참조하십시오(기본 Java 수명 주기 코드는 소프트웨어 개발 키트가 모바일 어트리뷰션과 성장을 촉진하는 방법 참조):
# 파일: app/src/main/keepRules/sdk-attribution.keep (또는 app/proguard-rules.pro)
# 중요: 예시일 뿐이며 OpoInstall SDK v2.1.0을 위한 검증된 프로덕션 규칙 세트가 아닙니다.
# 다운로드한 v2.1.0 아카이브 내부의 정확한 대소문자 구분 패키지 및 클래스 이름을 `jar tf` 또는 `apkanalyzer`를 통해 확인하고,
# 기존 소비자 규칙은 `configuration.txt`를 확인하십시오.
# ---------------------------------------------------------------------------
# 1. 명시적으로 유지된 클래스에 대한 바이트코드 속성 보존
# 제네릭 콜백 인터페이스(예: ResultCallBack<OpoData>)가 R8 전체 모드에서
# 매개변수화된 타입 메타데이터를 유지하기 위해 필요합니다.
# ---------------------------------------------------------------------------
-keepattributes Signature, InnerClasses, EnclosingMethod, *Annotation*
# ---------------------------------------------------------------------------
# 2. 범위 지정 SDK 진입점, 콜백 인터페이스 및 데이터 모델 규칙
# ---------------------------------------------------------------------------
# 공개 SDK 진입점과 리플렉티브하게 호출되는 모든 생성자/메서드 보존:
-keep public class com.opoinstall.api.OpoInstall {
<init>(...);
public <methods>;
}
# 콜백 리스너 인터페이스와 해당 콜백 메서드 보존:
-keep public interface com.opoinstall.api.ResultCallBack {
public <methods>;
}
# 모델 시나리오 A: OpoData / OpoError 모델이 리플렉션을 통해 동적으로 인스턴스화되거나
# 소거된 제네릭을 통해서만 참조되는 경우, -keep을 사용하여 클래스와 멤버를 모두 보존:
-keep class com.opoinstall.api.OpoData {
<init>(...);
<fields>;
public <methods>;
}
-keep class com.opoinstall.api.OpoError {
<init>(...);
<fields>;
public <methods>;
}
# 모델 시나리오 B: 모델 클래스가 호스트 호출 그래프에 이미 직접 유지되고 있고
# 해당 필드만 이름 변경으로부터 보호해야 하는 경우, -keepclassmembers 사용:
# -keepclassmembers class com.opoinstall.api.OpoData { <fields>; }
# ---------------------------------------------------------------------------
# 3. JNI 다운콜 및 네이티브-to-Java 업콜 보호 (예시용 JNI 패턴)
# ---------------------------------------------------------------------------
# JNI 케이스 A: Java/Kotlin 코드가 JNI 브리지 클래스를 이미 직접 인스턴스화하는 경우,
# C/C++ 업콜 메서드에는 -keepclassmembers,includedescriptorclasses를 사용하고,
# 네이티브 코드에 의해 인스턴스화된 매개변수 모델 클래스/생성자를 유지:
-keepclassmembers,includedescriptorclasses class com.example.sdk.internal.JniBridge {
public void onNativeAttributionEvent(com.example.sdk.internal.NativeEventData);
}
-keep class com.example.sdk.internal.NativeEventData {
<init>(...);
}
# JNI 케이스 B: 네이티브 C/C++ 코드가 FindClass / NewObject를 통해 Java 클래스 자체를
# 동적으로 조회하거나 인스턴스화하거나, 직접적인 Java 호출 사이트 없이
# RegisterNatives를 통해 네이티브 메서드를 등록하는 경우, -keep을 사용하여 클래스 및 메서드 제거 방지:
-keep,includedescriptorclasses class com.example.sdk.internal.DynamicNativeBridge {
<init>(...);
public native <methods>;
public void onNativeCallback(java.lang.String);
}
# 파일: app/proguard-rules.pro (진단 출력 지시문)
# 검증된 R8 구성 파일에 R8 진단 보고서 플래그를 배치하십시오.
# 경로는 모듈 작업 디렉토리(`app/`)를 기준으로 해석됩니다.
# 제품 변형(예: build/outputs/mapping/prodRelease/)에 맞게 디렉토리 이름을 조정하십시오.
-printseeds build/outputs/mapping/release/seeds.txt
-printusage build/outputs/mapping/release/usage.txt
-printconfiguration build/outputs/mapping/release/configuration.txt
Android 채널 패키지 난독화 및 하드닝 충돌 해결
빌드 타임 R8 컴파일, APK 정렬, 서명 및 채널 패키지 분리
Google Play 외부의 Android 배포를 위해 OpoInstall은 전용 Android 채널 패키지 내보내기를 지원합니다. 이는 개발자가 업로드한 기본 APK를 가져와서 별도의 channelCode 추적 메타데이터를 포함하는 채널별 APK를 생성하는 방식입니다.
누락된 채널 통계나 내보낸 채널 패키지의 설치 실패 문제를 해결할 때 엔지니어는 암호화된 APK 서명의 유효성과 벤더 채널 메타데이터 무결성이라는 두 가지 독립적인 릴리스 검증 기준을 분리해야 합니다:
- 빌드 타임 R8 코드 축소: R8은 기본 APK가 패키징되고 서명되기 전에 Java/Kotlin 바이트코드를
classes.dex로 컴파일할 때 Gradle 빌드 파이프라인 내부에서 실행됩니다. R8은 이미 내보낸 APK에서는 실행되지 않습니다. - Android APK 정렬(
zipalign) 및 암호화 서명(apksigner): AGP에 의해 빌드된 표준 릴리스 APK는 자동으로 정렬됩니다. 그러나 릴리스 파이프라인에서 서명 전에 사용자 지정 후처리나 타사 APK 보강(reinforcement)을 적용하는 경우, 공식 Androidzipalign사양과 Androidapksigner사양에 따라 APK를 다시 정렬해야 합니다.zipalign은 압축되지 않은 데이터를 4바이트 경계에 정렬하며, APK에 압축되지 않은 네이티브.so라이브러리가 포함된 경우 팀은 16KB 페이지 정렬 요구 사항을 충족하기 위해zipalign -P 16 -f -v 4 infile.apk outfile.apk를 사용하고zipalign -c -P 16 -v 4로 정렬을 검증해야 합니다.zipalign은apksigner를 통한 서명 전에 실행되어야 합니다. 서명 후 APK를 수정하면 V2/V3 전체 APK 암호화 서명이 무효화되기 때문입니다. - 벤더별 채널 메타데이터 및 서명 검증: OpoInstall 콘솔에서 채널 APK를 내보낸 후에 타사 APK 보강(패킹) 또는 재패키징 도구를 실행하면 포함된 채널 메타데이터가 덮어씌워지거나 APK 서명이 무효화될 수 있습니다. 타사 채널 패키지 파서가 특정 APK 서명 체계(예: V2/V3 전체 APK 서명과 함께 V1 JAR 서명 활성화)를 요구하는 경우, 프로덕션 서명 구성을 수정하기 전에 해당 요구 사항을 현재 OpoInstall 콘솔 문서나 벤더 지원을 통해 직접 확인하십시오. V1 서명을 보편적인 Android OS 요구 사항으로 간주하지 마십시오.
Android 채널 패키지에 권장되는 릴리스 파이프라인 순서
유효한 APK 서명, R8 바이트코드 최적화 및 온전한 벤더 채널 메타데이터를 유지하려면 다음 엄격한 순서로 릴리스 파이프라인을 실행하십시오:
- R8로 기본 APK 컴파일 및 미니파이: 빌드 타임에 R8이
classes.dex를 최적화하도록 검증된 유지 규칙과 함께./gradlew :app:assembleRelease를 실행하십시오. - 업로드 전 하드닝, 정렬 및 서명 완료: 보안 팀이 타사 APK 보강 도구를 사용하는 경우, 기본 APK에 적용하고,
zipalign -P 16 -f -v 4를 실행(서명 전에 재정렬이 필요한 경우)한 뒤,zipalign -c -P 16 -v 4로 정렬을 검증하고, 기본 APK에 서명하십시오. - 기본 APK를 업로드하고 내보낸 채널 APK를 독립적으로 검증: 서명되고 난독화된 기본 APK를 OpoInstall 콘솔에 업로드하고, 채널 APK를 내보낸 뒤, 배포 전에 두 가지 모두(암호화 서명 유효성(
apksigner verify --verbose) 및 물리적 기기에서의 채널 어트리뷰션)를 독립적으로 검증하십시오.
[권장 채널 패키지 파이프라인]: 소스 코드 -> Gradle R8 미니파이 -> (업로드 전 선택적 하드닝 + zipalign -P 16 + 서명) -> 기본 APK를 콘솔에 업로드 -> 채널 APK 내보내기 -> 서명(apksigner) 및 채널 콜백 모두 검증
R8 보고서 및 APK 분석기를 사용하여 보존된 SDK 심볼 감사
AGP 9.3+의 독립형 R8 구성 분석기 사용
Android Gradle Plugin 9.3 이상에서는 유지 규칙이 어떻게 병합되고 일치하는지 감사하기 위해 전체 릴리스 APK 또는 App Bundle 컴파일을 기다릴 필요가 없습니다. AGP 9.3+는 전용 analyze<Variant>R8Config Gradle 작업(예: ./gradlew :app:analyzeReleaseR8Config)을 제공합니다. 이 작업은 소비자 및 호스트 유지 규칙을 신속하게 평가하고 구성 진단을 출력하므로, 엔지니어는 전체 릴리스 빌드를 실행하기 전에 .keep 규칙을 반복적으로 수정할 수 있습니다.
configuration.txt, seeds.txt, usage.txt 및 mapping.txt 교차 확인

./gradlew :app:assembleRelease가 빌드 오류 없이 종료되었다고 해서 -keep 규칙이 작동했다고 가정하지 마십시오. 구성을 결정론적으로 검증하려면 app/build/outputs/mapping/<buildVariant>/에서 생성된 4개의 R8 진단 파일을 검사하십시오(특정 심볼이 유지된 이유를 디버깅할 때는 -whyareyoukeeping class <ClassName>을 사용하십시오):
configuration.txt: R8이 적용한 완전한 병합 규칙 세트를 포함합니다. 이는 호스트 유지 규칙, 기본 Android 플랫폼 규칙, AAPT2 규칙, 그리고 연결된.aar및.jar라이브러리 내부에 번들로 제공되는 모든/proguard.txt또는/META-INF/규칙을 결합한 것입니다.seeds.txt: 활성-keep규칙과 일치하는 모든 클래스, 생성자(<init>()), 메서드 및 필드를 나열합니다.seeds.txt에서의 일치는 규칙 구문을 확인해주지만, 반드시 DEX 검사 및 런타임 테스트와 병행해야 합니다.usage.txt: 트리 쉐이킹 중에 R8이 APK에서 제거한 죽은 코드를 나열합니다. 필수 SDK 클래스, 리플렉티브 생성자 또는 JNI 업콜 메서드 중 어느 것도usage.txt에 나타나지 않는지 확인하십시오.mapping.txt: 유지된 클래스 및 멤버에 대한 변경 전후 식별자 변환 맵을 기록합니다.mapping.txt에 없다고 해서 클래스가 원래 이름으로 보존된 것은 아닙니다. 축소 중에 완전히 제거되었을 수도 있습니다(이 경우usage.txt에 나열됨). 항상mapping.txt를seeds.txt,usage.txt및 컴파일된 DEX 파일과 교차 참조하십시오.
apkanalyzer를 사용하여 원시 DEX 식별자와 난독화 해제 보고서 상태 검증
공식 Google Android Developers APK Analyzer CLI를 사용하여 릴리스 APK를 검사할 때는 원시 컴파일된 DEX 식별자와 매핑이 해제된 이름을 구분해야 합니다:
- 원시 컴파일된 DEX 이름 검사 (
--proguard-mappings없이):apkanalyzer에--proguard-mappings mapping.txt를 전달하면, 이 도구는 난독화된 DEX 이름(예:a.b.c)을 화면 표시를 위해 원래 소스 이름으로 다시 변환합니다. 따라서--proguard-mappings가 활성화되었을 때 원래 클래스 이름이 보인다고 해서 해당 심볼이classes.dex에서 이름 변경을 피했다는 것을 증명하지는 않습니다. 먼저--proguard-mappings없이apkanalyzer dex packages를 실행하십시오. R8 클래스 재구성이 클래스를 평탄화된 패키지 경로로 이동시킬 수도 있으므로, 패키지grep에만 의존하지 마십시오. 원시 DEX 클래스 트리를 검사하고, 리플렉션 진입점 클래스 이름을 확인하고, JNI 메서드 이름과 설명자 서명을 확인하며, 필요한 메서드와 필드가 이름 변경되지 않았는지mapping.txt를 교차 확인하십시오. - R8 유지 상태 검사 (
--proguard-mappings,--proguard-seeds및--proguard-usages와 함께): 두 번째 패스에서는 세 가지 R8 보고서 파일을 모두 제공하여 모든 패키지(P), 클래스(C), 메서드(M) 및 필드(F) 전반에 걸쳐 유지 플래그(시드 규칙에 의해 유지됨은k, 축소 중 제거됨은x, 참조됨은r, 정의됨은d)를 재구성하십시오:
# Android 프로젝트 루트 디렉토리에서 실행 (제품 변형에 맞게 변형 경로 조정, 예: prodRelease)
# 1. (AGP 9.3+) 독립형 R8 구성 분석기를 실행하여 신속한 규칙 검증 후 릴리스 APK 빌드
./gradlew :app:analyzeReleaseR8Config
./gradlew :app:assembleRelease
# 2. 병합된 R8 구성, 일치하는 시드 및 제거된 사용량 보고서 검사
grep -i -C 3 "opoinstall" app/build/outputs/mapping/release/configuration.txt
grep -i "opoinstall" app/build/outputs/mapping/release/seeds.txt
grep -i "opoinstall" app/build/outputs/mapping/release/usage.txt
# 3A. 원시 컴파일된 DEX 식별자 검사 (--proguard-mappings 제외)
# 전체 원시 DEX 트리를 검사하고 R8이 클래스를 재구성했을 경우를 대비해 mapping.txt도 교차 확인
apkanalyzer dex packages app/build/outputs/apk/release/app-release.apk | grep -i "opoinstall"
# 3B. R8 유지 상태 플래그 검사 (k = 유지됨, x = 제거됨, r = 참조됨, d = 정의됨) R8 보고서 파일 사용
apkanalyzer dex packages \
--proguard-mappings app/build/outputs/mapping/release/mapping.txt \
--proguard-seeds app/build/outputs/mapping/release/seeds.txt \
--proguard-usages app/build/outputs/mapping/release/usage.txt \
app/build/outputs/apk/release/app-release.apk | grep -i "opoinstall"
# 4. 16KB 페이지 정렬 검증(네이티브 .so 파일이 있을 경우) 및 암호화 APK 서명 검증
zipalign -c -P 16 -v 4 app/build/outputs/apk/release/app-release.apk
apksigner verify --verbose app/build/outputs/apk/release/app-release.apk
릴리스 빌드 어트리뷰션 문제 해결 매트릭스
미니파이된 Android 빌드에서 보이지 않는 어트리뷰션 및 딥링크 실패 진단
미니파이된 릴리스 빌드에서만 발생하는 어트리뷰션 불일치 문제를 해결할 때 아래의 진단 매트릭스를 사용하여 근본 원인을 분리하십시오:
| 관찰된 릴리스 증상 | 가능성 있는 R8 / 빌드 근본 원인 | 진단 검증 단계 | 수정 조치 |
|---|---|---|---|
릴리스 빌드에서 getInstallParam 또는 handleDeepLink 콜백이 실행되지 않음 |
패키지 불일치로 인해 SDK 진입점 클래스, 리플렉티브 <init>() 생성자 또는 ResultCallBack 메서드가 제거되거나 이름이 변경됨 |
seeds.txt와 usage.txt 확인; 원시 apkanalyzer dex packages 출력(--proguard-mappings 없이) 및 mapping.txt 검사 |
jar tf를 사용하여 .jar / .aar을 검사하여 정확한 대소문자 구분 클래스 경로를 찾고 <init>(...);를 포함한 범위 지정 -keep 규칙 추가 |
콜백은 실행되지만 OpoData 사용자 지정 매개변수가 비어 있거나 null로 파싱됨 |
OpoData가 -keepclassmembers로만 보호되어 클래스 자체가 제거되었거나 제네릭 Signature 메타데이터가 손실됨 |
OpoData에 대한 usage.txt 및 mapping.txt 확인, configuration.txt에서 -keepattributes Signature 확인 |
<init>(...); <fields>;와 함께 -keep class ...(OpoData가 리플렉티브하게 인스턴스화되는 경우)를 사용하고 -keepattributes Signature 유지 |
SDK가 .so 라이브러리를 로드할 때 UnsatisfiedLinkError 또는 네이티브 콜백 누락 |
동적으로 등록된 RegisterNatives 다운콜이 축소되었거나 C/C++-to-Java 업콜 대상 메서드 이름이 R8에 의해 변경됨 |
logcat에서 정확한 누락 JNI 심볼을 검사하고 대상 Java 클래스에 대해 원시 apkanalyzer dex packages 및 mapping.txt 확인 |
JNI 업콜 및 RegisterNatives 대상에 대해 includedescriptorclasses와 함께 -keep(또는 브리지 클래스가 Java에서 직접 인스턴스화되는 경우 -keepclassmembers) 사용 |
| 내보낸 Android 채널 패키지가 설치되지 않거나 채널 통계 누락 | 내보내기 후 APK 수정으로 인해 APK 서명이 무효화되었거나 내장 채널 메타데이터가 덮어씌워짐 | 내보낸 채널 APK에서 zipalign -c -P 16 -v 4 및 apksigner verify --verbose 실행 후 물리적 기기에서 테스트 |
콘솔에 업로드하기 전에 기본 APK에서 R8 미니파이, 하드닝, 정렬 및 서명을 모두 완료하고, 내보내기 후 서명과 어트리뷰션을 모두 검증 |
자주 묻는 질문 (FAQ)
모바일 SDK를 Android ProGuard 및 R8 난독화에서 어떻게 제외하나요?
Android SDK 클래스가 R8 난독화에서 제외되었는지 어떻게 확인하나요?
APK 하드닝이나 후처리 후 Android 채널 패키지 통계가 작동하지 않는 이유는 무엇인가요?
요약 및 릴리스 빌드 검증 체크리스트
Android ProGuard 및 R8 난독화에서 모바일 SDK를 제외하는 방법을 이해하는 것은 디버그 테스트에서 프로덕션 릴리스 빌드로 전환할 때 보이지 않는 어트리뷰션 회귀를 방지하는 데 필수적입니다. 번들된 소비자 규칙을 검증하고, SDK 아카이브 내부의 정확한 대소문자 구분 클래스 경로와 일치하는 좁은 범위의 -keep 지시문을 작성하며, Android 채널 패키지에 대한 업로드 전 빌드 순서를 존중하고, R8 진단 출력 파일을 원시 DEX 바이트코드와 교차 확인함으로써, Android 엔지니어는 지연된 딥링크나 매개변수 복원을 중단시키지 않으면서도 전체 코드 축소를 유지할 수 있습니다.
미니파이된 Android 릴리스 빌드를 Google Play에 게시하거나 채널 패키지를 내보내기 전에 다음 4단계 검증 체크리스트를 실행하십시오:
-
SDK 아카이브 및 병합된 구성 검사:
jar tf또는unzip -l로 다운로드한 SDK 아카이브(예: v2.1.0)를 확인하여 정확한 클래스 경로와 번들된 소비자 규칙을 검사하고,./gradlew :app:analyzeReleaseR8Config(AGP 9.3+에서)를 실행하거나configuration.txt를 검토하십시오. -
R8 보고서 및 원시 DEX 식별자 교차 확인: 필수 SDK 진입점, 리플렉티브
<init>()생성자,OpoData모델 필드,ResultCallBack메서드 및 JNI 서명이seeds.txt에 나타나고usage.txt에는 없으며, 원시apkanalyzer dex packages출력(--proguard-mappings없이 실행)과mapping.txt에서 원래 이름을 유지하는지 확인하십시오. -
업로드 전 미니파이 강제 및 채널 APK 서명 검증: Android 채널 패키지를 내보내기 위해 기본 APK를 업로드하기 전에 R8 미니파이, 모든 타사 하드닝,
zipalign(zipalign -c -P 16 -v 4) 및 APK 서명을 모두 완료하고, 내보낸 패키지에서apksigner verify --verbose를 실행하십시오. -
물리적 기기 릴리스 스모크 테스트 실행: 서명되고 미니파이된 릴리스 APK를 실제 Android 기기에 설치하여 H5 매개변수 설치(
getInstallParam)와 원클릭 웨이크업(handleDeepLink)을 모두 검증하십시오.
엔지니어링 팀은 OpoInstall 개발자 콘솔에서 애플리케이션을 등록하고 난독화된 기본 APK를 업로드하여 채널 패키지 내보내기를 테스트하고 릴리스 빌드 매개변수 복원을 검증할 수 있습니다.
관련 자료
-
개념: Android R8 코드 축소, 식별자 난독화, 소비자 ProGuard 규칙, 채널 패키지 빌드 순서
-
기술: Android Gradle Plugin (AGP), R8 컴파일러,
proguard-rules.pro, Android Studio APK Analyzer -
플랫폼 및 통합 인터페이스:
isMinifyEnabled,optimization.enable,analyzeReleaseR8Config,consumerProguardFiles,seeds.txt,usage.txt,apkanalyzer dex packages,apksigner,zipalign -
공식 문서 및 참조:
Share this article



