개요
안드로이드 애플리케이션 개발 시 APK 크기를 줄이고 코드 역공학을 방지하기 위한 기술로 ProGuard 코드 난독화가 널리 사용됩니다. 이 글에서는 ProGuard의 기본 개념부터 실제 적용 방법까지 상세히 다룹니다.
ProGuard의 네 가지 핵심 기능
ProGuard는 다음과 같은 네 단계의 처리 과정을 거칩니다:
- 코드 축소(Shrink): 사용하지 않는 클래스, 변수, 메서드, 속성을 제거하여 APK 크기를 줄입니다.
- 코드 최적화(Optimize): 메서드 바이트코드를 최적화하고 사용하지 않는 생성자를 제거합니다.
- 코드 난독화(Obfuscate): 의미 있는 이름을 무의미한 이름으로 변경하여 코드 분석을 어렵게 만듭니다.
- 사전 검증(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: 리소스 최적화 기록 파일