안드로이드 ProGuard 코드 난독화 가이드

개요

안드로이드 애플리케이션 개발 시 APK 크기를 줄이고 코드 역공학을 방지하기 위한 기술로 ProGuard 코드 난독화가 널리 사용됩니다. 이 글에서는 ProGuard의 기본 개념부터 실제 적용 방법까지 상세히 다룹니다.

ProGuard의 네 가지 핵심 기능

ProGuard는 다음과 같은 네 단계의 처리 과정을 거칩니다:

  1. 코드 축소(Shrink): 사용하지 않는 클래스, 변수, 메서드, 속성을 제거하여 APK 크기를 줄입니다.
  2. 코드 최적화(Optimize): 메서드 바이트코드를 최적화하고 사용하지 않는 생성자를 제거합니다.
  3. 코드 난독화(Obfuscate): 의미 있는 이름을 무의미한 이름으로 변경하여 코드 분석을 어렵게 만듭니다.
  4. 사전 검증(Preverify): 클래스에 사전 검증 정보를 추가합니다(Java 6 이상 및 J2ME 요구 사항).

기본 구문 규칙

입력/출력 옵션

@ 파일명

'파일명'의 약식 표현으로 '-include 파일명'과 동일합니다.

-include 파일명

지정된 파일에서 재귀적으로 구성 옵션을 읽어옵니다.

-basedirectory 디렉토리명

이 구성 매개변수 또는 이 구성 파일의 모든 후속 상대 파일 이름에 대한 기본 디렉토리를 지정합니다.

-injars 클래스_경로

처리할 애플리케이션의 입력 jar(또는 apks, aabs, aars, wars, ears, jmods, zip 또는 디렉토리)를 지정합니다. 여러 개의 -injars 옵션을 사용하여 클래스 경로 항목을 지정할 수 있습니다.

-outjars 클래스_경로

출력 jar의 이름(또는 apks, aabs, aars, wars, ears, jmods, zips 또는 디렉토리)을 지정합니다. 가독성을 높이기 위해 여러 개의 -outjars 옵션을 사용하여 클래스 경로 항목을 지정할 수 있습니다. -outjars 옵션이 없으면 jar가 생성되지 않습니다.

-libraryjars 클래스_경로

처리할 애플리케이션의 라이브러리 jar(또는 apks, aabs, aars, wars, ears, jmods, zips, 디렉토리)를 지정합니다.

보존 옵션

-keep [,수정자,...] 클래스_명세
-keepnames 클래스_명세
코드 진입점으로 유지할 클래스 및 클래스 멤버(필드와 메서드)를 지정합니다.

-keepclassmembers [,수정자,...] 클래스_명세
-keepclassmembernames 클래스_명세
해당 클래스도 유지되는 경우 유지할 클래스 멤버(필드와 메서드)를 지정합니다.

-keepclasseswithmembers [,수정자,...] 클래스_명세
-keepclasseswithmembernames 클래스_명세
지정된 모든 클래스 멤버가 존재하는 경우 유지할 클래스 및 클래스 멤버를 지정합니다.
-if 클래스_명세

다양한 -keep 옵션과 일치하는 클래스 및 클래스 멤버를 활성화해야 함을 지정합니다. 조건과 후속 keep 옵션은 와일드카드와 와일드카드 참조를 공유할 수 있습니다. 예를 들어, Dagger 및 Butterknife와 같은 프레임워크 클래스를 프로젝트에 관련 이름의 클래스가 존재하는 경우에 유지할 수 있습니다.

-printseeds [파일명]

다양한 -keep 옵션과 일치하는 클래스 및 클래스 멤버를 상세하게 나열하도록 지정합니다. 이 목록은 표준 출력 또는 지정된 파일에 인쇄됩니다. 이 목록은 예상대로 클래스 멤버를 찾았는지 검증하는 데 사용할 수 있으며, 특히 와일드카드를 사용하는 경우에 유용합니다.

축소 옵션

-dontshrink

입력을 축소하지 않도록 지정합니다. 기본적으로 ProGuard는 코드를 축소합니다: 사용하지 않는 모든 클래스 및 클래스 멤버를 삭제합니다. 다양한 -keep 옵션에 나열된 항목과 직접 또는 간접적으로 의존하는 항목만 유지합니다. 또한 각 최적화 단계 후 축소 단계를 적용하여 일부 최적화가 더 많은 클래스 및 클래스 멤버를 삭제할 가능성을 제공합니다.

-printusage [파일명]

입력 클래스 파일의 사용되지 않는 코드를 나열하도록 지정합니다. 이 목록은 표준 출력 또는 지정된 파일에 인쇄됩니다. 예를 들어, 애플리케이션에서 사용하지 않는 코드를 나열할 수 있습니다. 축소 시에만 적용됩니다.

최적화 옵션

-dontoptimize

입력 클래스 파일을 최적화하지 않도록 지정합니다. 기본적으로 ProGuard는 모든 코드를 최적화합니다. 클래스 및 클래스 멤버를 인라인하고 병합하며 바이트코드 수준에서 모든 메서드를 최적화합니다.

-optimizations 최적화_필터

활성화 및 비활성화할 최적화를 더 세분화된 수준으로 지정합니다. 최적화 시에만 적용됩니다.

-assumenosideeffects 클래스_명세

반환 값 외에는 부작용이 없는 메서드를 지정합니다. 예를 들어, System.currentTimeMillis() 메서드는 값을 반환하지만 부작용은 없습니다. 최적화 단계에서 ProGuard가 반환 값이 사용되지 않는다고 판단할 수 있는 경우 이러한 메서드 호출을 삭제할 수 있습니다. ProGuard는 프로그램 코드를 분석하여 이러한 메서드를 자동으로 찾습니다. 라이브러리 코드는 분석하지 않으므로 이 옵션이 유용합니다.

난독화 옵션

-dontobfuscate

입력 클래스 파일을 난독화하지 않도록 지정합니다. 기본적으로 ProGuard는 코드를 난독화합니다: 클래스 및 클래스 멤버에 새로운 짧은 임의의 이름을 할당합니다. 소스 파일 이름, 변수 이름 및 줄 번호와 같이 디버깅에만 유용한 내부 속성을 삭제합니다.

-keepattributes [속성_필터]

유지할 모든 선택적 속성을 지정합니다. 하나 이상의 -keepattributes 명령을 사용하여 속성을 지정할 수 있습니다.

# 예외 발생 시 코드 줄 번호 유지
-keepattributes SourceFile,LineNumberTable
# 코드의 주석을 난독화에서 보호
-keepattributes *Annotation*
# 제네릭 혼동 방지 - JSON 엔티티 매핑 중요
-keepattributes Signature

필터 및 패턴

?	이름의 단일 문자와 일치합니다. 예를 들어, "com.example.Test?"는 "com.example.Test1" 및 "com.example.Test2"와 일치하지만 "com.example.Test12"와는 일치하지 않습니다.

*	패키지 구분자 또는 디렉토리 구분자를 포함하지 않는 이름의 모든 부분과 일치합니다. 
예를 들어, "com.example.*Test*"는 "com.example.Test" 및 "com.example.YourTestApplication"과 일치하지만 "com.example.mysubpackage.MyTest"와는 일치하지 않습니다.

**	패키지 구분자 또는 디렉토리 구분자를 포함할 수 있는 이름의 모든 부분과 일치합니다.

<n>	동일한 옵션에서 n번째 일치하는 와일드카드와 일치합니다. 예를 들어, "com.example.*Foo<1>"은 "com.example.BarFooBar"와 일치합니다.

사용되지 않는 코드 찾기

사용되지 않는 코드 및 메서드를 식별하려면 다음 옵션을 사용할 수 있습니다:

# 최적화 비활성화
-dontoptimize
# 난독화 비활성화
-dontobfuscate
# 사전 검증 비활성화 - Android에는 필요하지 않으므로 난독화 속도 향상
-dontpreverify
# 사용되지 않는 코드 목록 출력
-printusage

이 구성을 적용하고 빌드하면 다음과 같은 출력이 표시되어 사용되지 않은 클래스 및 메서드를 나열합니다:

org.fmod.example.DeadClass
org.fmod.example.MemoryShakeActivity:
    public void dpOperate()
    public void testa()
    public void testb()

난독화 기본 원칙

시스템 관련 클래스는 난독화하지 않아야 합니다

  • 네 가지 주요 구성 요소: Activity, Service, Provider, Broadcast 및 그들의 Fragment와 같은 시스템 관련 클래스
# Activity, Application, Service, BroadcastReceiver, ContentProvider 등을 상속하는 클래스는 난독화하지 않음
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class * extends android.support.v4.app.Fragment
-keep public class * extends androidx.fragment.app.Fragment
-keep public class * extends android.content.ContentProvider
  • View 관련 클래스:
# Activity의 모든 View 메서드 유지
-keepclassmembers class * extends android.app.Activity{
    public void *(android.view.View);
}
# View의 setXxx() 및 getXxx() 메서드 유지
# 속성 애니메이션에는 해당 setter 및 getter 메서드 구현이 필요
-keep public class * extends android.view.View{
    *** get*();
    void set*(***);
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
}
  • 열거형 클래스:
# 열거형의 values() 및 valueOf() 메서드 유지
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}
  • 주석(Annotation)
  • Support 라이브러리의 모든 클래스 및 내부 클래스
-keep class android.support.** {*;}
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**
  • Serializable을 구현하는 클래스:
# Serializable을 구현하는 클래스의 다음 멤버는 제거 및 난독화하지 않음
-keepclassmembers class * implements java.io.Serializable {
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
}
  • Parcelable의 하위 클래스 및 Creator 정적 멤버 변수:
# Parcelable 구현 클래스의 CREATOR 필드 유지
-keep class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator *;
}

프로젝트 관련 일부 클래스는 난독화하지 않아야 합니다

  • 데이터 모델(GSON, fastjson 등 프레임워크가 서버 데이터를 파싱할 때 사용)
-keep class com.yourcompany.entity.** {*;}
  • JNI 인터페이스와 Java 네이티브 메서드(이 메서드는 네이티브 메서드와 일치해야 함, 난독화하면 찾을 수 없어 오류 발생)
-keepclasseswithmembernames class * {
    native <methods>;
}
  • Java 인터페이스(주로 외부에 공개되는 인터페이스)
  • 리플렉션을 사용하는 곳
  • 추상 내부 클래스(예: Animal의 내부 클래스)
-keep class com.yourcompany.demo.Animal{*;}
-keep class com.yourcompany.demo.Animal$*{*;}
  • 사용하는 오픈소스 라이브러리나 타사 SDK 패키지
# WeChat 결제
-keep class com.tencent.mm.opensdk.** {*; }

난독화 사전 추가

난독화를 더 복잡하게 만들기 위해 사용자 정의 난독화 사전을 추가할 수 있습니다:

-classobfuscationdictionary proguard-dic.txt  # 클래스 사전
-obfuscationdictionary proguard-dic.txt        # 난독화 사전
-packageobfuscationdictionary proguard-dic.txt # 패키지 사전

R8과 ProGuard

R8 대신 ProGuard를 사용하려면 gradle.properties 파일에 다음 구성을 추가하여 R8을 비활성화할 수 있습니다:

android.enableR8=false
android.enableR8.libraries=false

Android 빌드 후 다음과 같은 파일이 생성됩니다:

  • mapping.txt: 원본과 난독화된 클래스, 메서드, 필드 이름 간의 변환 관계
  • seeds.txt: 난독화되지 않은 클래스 및 멤버
  • usage.txt: APK에서 제거된 코드(사용되지 않는 코드)
  • configuration.txt: 구성 정보
  • missing_rules.txt: 누락된 규칙
  • resources.txt: 리소스 최적화 기록 파일

태그: 안드로이드 코드 난독화 proguard R8 APK 최적화

6월 1일 12:01에 게시됨