반응형
Android ProGuard 완벽 가이드 - 소스코드 난독화와 최적화 방법
Android 앱 개발자라면 꼭 알아야 할 ProGuard는 자바 바이트코드를 최적화하고 난독화하는 필수 도구입니다. 앱의 보안을 강화하고 크기를 최적화하며, 리버스 엔지니어링을 방지하는 ProGuard의 모든 것을 이번 포스팅에서 상세히 알아보겠습니다. 특히 ProGuard 설정에 필요한 중요 파일 목록과 그 내용, build.gradle 설정 방법, 라이브러리별 ProGuard 규칙, 그리고 실전에서 자주 발생하는 문제 해결 방법까지 초보자도 쉽게 따라할 수 있도록 단계별로 설명합니다. 또한 Android Studio 3.4 이후 기본으로 사용되는 R8과 ProGuard의 차이점도 함께 다루어, 최신 안드로이드 개발 환경에서 코드 난독화를 효과적으로 적용하는 방법을 배울 수 있습니다.

목차
1. ProGuard 개념과 필요성
2. ProGuard 설정과 주요 파일
3. proguard-rules.pro 작성 가이드
4. 라이브러리별 규칙과 문제 해결
5. 자주 묻는 질문 (FAQ)
#1. ProGuard 개념과 필요성
ProGuard는 Google이 Android Studio에 기본으로 통합한 자바 바이트코드 최적화 및 난독화 도구입니다. 앱의 보안 강화, 용량 감소, 성능 최적화라는 세 가지 핵심 목표를 달성하는 데 필수적인 역할을 합니다.
1) ProGuard가 필요한 이유

Android 앱 배포 시 ProGuard를 적용해야 하는 네 가지 핵심 이유를 알아보겠습니다.
(1) 리버스 엔지니어링 방지
APK 파일은 쉽게 디컴파일할 수 있어 소스코드가 그대로 노출될 위험이 있습니다. ProGuard는 클래스, 메서드, 변수 이름을 의미 없는 짧은 문자로 변경하여 코드 분석을 어렵게 만듭니다.
// ProGuard 적용 전
public class UserAuthentication {
private String secretKey = "my_secret_key_12345";
public boolean validateUser(String username, String password) {
return encryptPassword(password).equals(secretKey);
}
}
// ProGuard 적용 후 (난독화됨)
public class a {
private String b = "my_secret_key_12345";
public boolean a(String c, String d) {
return b(d).equals(b);
}
}
public class UserAuthentication {
private String secretKey = "my_secret_key_12345";
public boolean validateUser(String username, String password) {
return encryptPassword(password).equals(secretKey);
}
}
// ProGuard 적용 후 (난독화됨)
public class a {
private String b = "my_secret_key_12345";
public boolean a(String c, String d) {
return b(d).equals(b);
}
}
(2) APK 크기 감소
ProGuard는 사용하지 않는 코드, 클래스, 필드, 메서드를 제거하여 APK 파일 크기를 대폭 줄입니다. 또한 이름을 짧게 변경하여 추가로 용량을 절약합니다.
| 항목 | ProGuard 적용 전 | ProGuard 적용 후 | 감소율 |
|---|---|---|---|
| APK 크기 | 25.3 MB | 18.7 MB | 약 26% 감소 |
| 메서드 수 | 45,820개 | 32,150개 | 약 30% 감소 |
| 클래스 수 | 6,254개 | 4,183개 | 약 33% 감소 |
(3) 성능 최적화
ProGuard는 바이트코드를 분석하고 최적화하여 앱 실행 속도를 향상시킵니다. 주요 최적화 기법은 다음과 같습니다.
① 메서드 인라이닝 - 짧은 메서드를 호출 위치에 직접 삽입하여 호출 오버헤드 제거
② 상수 계산 - 컴파일 타임에 계산 가능한 값을 미리 계산
③ 불필요한 코드 제거 - 실행되지 않는 if 문, 미사용 변수 등 제거
④ 필드 접근 최적화 - getter/setter를 직접 필드 접근으로 변환
② 상수 계산 - 컴파일 타임에 계산 가능한 값을 미리 계산
③ 불필요한 코드 제거 - 실행되지 않는 if 문, 미사용 변수 등 제거
④ 필드 접근 최적화 - getter/setter를 직접 필드 접근으로 변환
(4) DEX 65K 메서드 제한 문제 해결
Android의 DEX 파일 형식은 단일 DEX 파일당 최대 65,536개의 메서드만 참조할 수 있습니다. ProGuard는 미사용 메서드를 제거하여 이 제한을 피하거나 완화할 수 있습니다.
. . . . .
2) ProGuard의 네 가지 핵심 기능
ProGuard는 네 단계의 처리 과정을 통해 코드를 최적화합니다.
(1) 축소 (Shrinking)
사용하지 않는 클래스, 필드, 메서드를 감지하고 제거합니다. Entry Point(Activity, Service 등)로부터 시작하여 도달 가능한 코드만 남기고 나머지는 모두 삭제합니다.
// 사용되지 않는 유틸리티 클래스
public class UnusedUtils {
public static void neverCalled() {
// 이 메서드는 어디서도 호출되지 않음
System.out.println("This will be removed");
}
}
// ProGuard가 이 클래스와 메서드를 완전히 제거함
public class UnusedUtils {
public static void neverCalled() {
// 이 메서드는 어디서도 호출되지 않음
System.out.println("This will be removed");
}
}
// ProGuard가 이 클래스와 메서드를 완전히 제거함
(2) 최적화 (Optimization)
바이트코드를 분석하고 최적화하여 실행 속도를 향상시킵니다. 이 단계에서는 메서드 인라이닝, 상수 계산, 불필요한 코드 블록 제거 등을 수행합니다.
(3) 난독화 (Obfuscation)
클래스, 필드, 메서드의 이름을 의미 없는 짧은 이름으로 변경합니다. 이는 디컴파일된 코드를 이해하기 어렵게 만들어 리버스 엔지니어링을 방지합니다.
예를 들어, UserManager 클래스는 'a'로, getUserName() 메서드는 'b()'로 변경됩니다. 또한 패키지 이름도 간소화되어 com.example.myapp.user.UserManager는 a.b.c.a가 될 수 있습니다.
(4) 사전 검증 (Preverification)
자바 바이트코드를 JVM이나 Android 런타임에 로드하기 전에 미리 검증합니다. 이는 클래스 로딩 시간을 단축하고 런타임 오버헤드를 줄입니다.
. . . . .
3) R8과 ProGuard의 관계
Android Studio 3.4부터는 기본적으로 ProGuard 대신 R8 컴파일러가 사용됩니다. R8은 Google이 개발한 차세대 코드 축소 및 난독화 도구입니다.
| 비교 항목 | ProGuard | R8 |
|---|---|---|
| 개발사 | GuardSquare | |
| 처리 속도 | 상대적으로 느림 | ProGuard보다 빠름 |
| APK 크기 | 효과적으로 감소 | 더 작은 APK 생성 |
| 규칙 호환성 | - | ProGuard 규칙 호환 |
| 최적화 수준 | 우수 | 더 공격적인 최적화 |
| 기본 사용 | Android Studio 3.3 이하 | Android Studio 3.4 이상 |
중요: R8은 ProGuard 규칙 파일(.pro)을 그대로 사용하므로, 이번 가이드의 모든 내용이 R8에도 동일하게 적용됩니다.
#2. ProGuard 설정과 주요 파일
ProGuard를 효과적으로 사용하려면 관련 설정 파일과 빌드 과정에서 생성되는 파일들을 이해해야 합니다.
1) build.gradle 파일 설정
앱 수준의 build.gradle 파일에서 ProGuard를 활성화하는 방법을 알아보겠습니다.
(1) 기본 설정
// app/build.gradle
android {
buildTypes {
release {
// 코드 축소, 난독화, 최적화 활성화
minifyEnabled true
// 사용하지 않는 리소스 제거
shrinkResources true
// ProGuard 규칙 파일 지정
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
debug {
// 디버그 빌드는 일반적으로 ProGuard 비활성화
minifyEnabled false
}
}
}
android {
buildTypes {
release {
// 코드 축소, 난독화, 최적화 활성화
minifyEnabled true
// 사용하지 않는 리소스 제거
shrinkResources true
// ProGuard 규칙 파일 지정
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
debug {
// 디버그 빌드는 일반적으로 ProGuard 비활성화
minifyEnabled false
}
}
}
(2) 주요 설정 옵션 상세 설명
| 옵션 | 설명 | 권장 사용 |
|---|---|---|
| minifyEnabled | 코드 축소, 최적화, 난독화 활성화 | Release에서 true |
| shrinkResources | 사용하지 않는 리소스 파일 제거 (이미지, XML 등) | Release에서 true |
| proguardFiles | 적용할 ProGuard 규칙 파일 목록 지정 | 기본 파일 + 커스텀 규칙 |
주의: shrinkResources는 minifyEnabled가 true일 때만 작동합니다. 리소스 축소는 코드 축소 이후에 사용되지 않는 리소스를 식별하기 때문입니다.
(3) R8 사용 제어
// gradle.properties 파일
# R8 사용 (Android Studio 3.4+ 기본값)
android.enableR8=true
# 전체 모드 R8 (더 공격적인 최적화)
android.enableR8.fullMode=true
# ProGuard로 되돌리기 (권장하지 않음)
# android.enableR8=false
# R8 사용 (Android Studio 3.4+ 기본값)
android.enableR8=true
# 전체 모드 R8 (더 공격적인 최적화)
android.enableR8.fullMode=true
# ProGuard로 되돌리기 (권장하지 않음)
# android.enableR8=false
. . . . .
2) 중요 ProGuard 파일 목록
ProGuard 관련 파일들은 크게 설정 파일과 출력 파일로 나뉩니다.
(1) 설정 파일
① proguard-android.txt
위치: <ANDROID_SDK>/tools/proguard/proguard-android.txt
설명: Android SDK에서 제공하는 기본 ProGuard 설정
내용: Android 프레임워크 관련 기본 keep 규칙
사용: getDefaultProguardFile('proguard-android.txt')
설명: Android SDK에서 제공하는 기본 ProGuard 설정
내용: Android 프레임워크 관련 기본 keep 규칙
사용: getDefaultProguardFile('proguard-android.txt')
② proguard-android-optimize.txt
위치: <ANDROID_SDK>/tools/proguard/proguard-android-optimize.txt
설명: 최적화가 추가된 향상된 버전
내용: proguard-android.txt + 추가 최적화 옵션
사용: getDefaultProguardFile('proguard-android-optimize.txt')
특징: 더 공격적인 최적화로 APK 크기 추가 감소
설명: 최적화가 추가된 향상된 버전
내용: proguard-android.txt + 추가 최적화 옵션
사용: getDefaultProguardFile('proguard-android-optimize.txt')
특징: 더 공격적인 최적화로 APK 크기 추가 감소
③ proguard-rules.pro
위치: app/proguard-rules.pro
설명: 앱별 커스텀 ProGuard 규칙 정의 파일
내용: 라이브러리 규칙, 커스텀 클래스 유지 규칙 등
편집: Android Studio에서 직접 편집 가능
중요도: 가장 자주 수정하는 파일
설명: 앱별 커스텀 ProGuard 규칙 정의 파일
내용: 라이브러리 규칙, 커스텀 클래스 유지 규칙 등
편집: Android Studio에서 직접 편집 가능
중요도: 가장 자주 수정하는 파일
(2) 출력 파일 (빌드 후 생성)
ProGuard 처리 과정에서 생성되는 네 가지 중요한 파일이 있으며, 모두 app/build/outputs/mapping/release/ 디렉토리에 저장됩니다.
① mapping.txt
설명: 난독화 전후 이름 매핑 정보
예시 내용:
com.example.app.UserManager -> a.b.c.a:
java.lang.String userName -> a
void setUserName(java.lang.String) -> a
java.lang.String getUserName() -> b
중요성: 크래시 리포트 분석에 필수!
관리: 각 릴리스 버전마다 별도 보관 필요
예시 내용:
com.example.app.UserManager -> a.b.c.a:
java.lang.String userName -> a
void setUserName(java.lang.String) -> a
java.lang.String getUserName() -> b
중요성: 크래시 리포트 분석에 필수!
관리: 각 릴리스 버전마다 별도 보관 필요
mapping.txt 파일은 반드시 버전별로 보관해야 합니다! 이 파일 없이는 사용자로부터 받은 크래시 리포트를 분석할 수 없습니다.
② seeds.txt
설명: 난독화되지 않고 유지된 클래스와 멤버 목록
용도: Entry Point로 보존된 항목 확인
예시 내용:
com.example.app.MainActivity
com.example.app.model.User
android.support.v4.app.Fragment
용도: Entry Point로 보존된 항목 확인
예시 내용:
com.example.app.MainActivity
com.example.app.model.User
android.support.v4.app.Fragment
③ usage.txt
설명: 제거된 코드와 사용된 코드 목록
용도: 어떤 코드가 제거되었는지 확인
활용: 의도치 않게 제거된 클래스 발견
용도: 어떤 코드가 제거되었는지 확인
활용: 의도치 않게 제거된 클래스 발견
④ dump.txt
설명: 앱의 내부 구조 상세 정보
내용: 모든 클래스 파일의 구조를 텍스트로 표현
용도: 고급 디버깅과 문제 해결
내용: 모든 클래스 파일의 구조를 텍스트로 표현
용도: 고급 디버깅과 문제 해결
. . . . .
3) Firebase Crashlytics 매핑 파일 업로드
ProGuard를 사용하면 크래시 리포트도 난독화됩니다. Firebase Crashlytics를 사용한다면 mapping.txt를 자동으로 업로드하도록 설정해야 합니다.
// app/build.gradle
android {
buildTypes {
release {
minifyEnabled true
// Crashlytics 매핑 파일 자동 업로드
firebaseCrashlytics {
mappingFileUploadEnabled true
}
}
}
}
android {
buildTypes {
release {
minifyEnabled true
// Crashlytics 매핑 파일 자동 업로드
firebaseCrashlytics {
mappingFileUploadEnabled true
}
}
}
}
이렇게 설정하면 릴리스 빌드 시 자동으로 mapping.txt 파일이 Firebase 서버에 업로드되어, 크래시 리포트를 볼 때 자동으로 원본 클래스/메서드 이름으로 변환됩니다.
#3. proguard-rules.pro 작성 가이드
proguard-rules.pro 파일은 앱 특성에 맞게 개발자가 직접 작성하는 가장 중요한 설정 파일입니다. 올바른 규칙 작성이 ProGuard 성공의 핵심입니다.
1) 기본 템플릿과 필수 규칙
모든 Android 앱에 공통으로 적용할 수 있는 기본 템플릿입니다.
(1) 기본 Attribute 보존
# ========== 기본 Attribute 보존 ==========
# Signature: 제네릭 타입 정보 유지 (Retrofit, Gson 등에 필요)
-keepattributes Signature
# Annotation: 런타임 어노테이션 정보 유지
-keepattributes *Annotation*
# 스택 트레이스에 소스 파일명과 라인 번호 유지 (디버깅용)
-keepattributes SourceFile,LineNumberTable
# Exception 클래스 유지
-keep public class * extends java.lang.Exception
# Signature: 제네릭 타입 정보 유지 (Retrofit, Gson 등에 필요)
-keepattributes Signature
# Annotation: 런타임 어노테이션 정보 유지
-keepattributes *Annotation*
# 스택 트레이스에 소스 파일명과 라인 번호 유지 (디버깅용)
-keepattributes SourceFile,LineNumberTable
# Exception 클래스 유지
-keep public class * extends java.lang.Exception
(2) Android 컴포넌트 보존
# ========== Android 컴포넌트 ==========
# Activity 보존
-keep public class * extends android.app.Activity
-keep public class * extends androidx.appcompat.app.AppCompatActivity
# Application 클래스 보존
-keep public class * extends android.app.Application
# Service 보존
-keep public class * extends android.app.Service
# BroadcastReceiver 보존
-keep public class * extends android.content.BroadcastReceiver
# ContentProvider 보존
-keep public class * extends android.content.ContentProvider
# Fragment 보존
-keep public class * extends android.app.Fragment
-keep public class * extends androidx.fragment.app.Fragment
# Activity 보존
-keep public class * extends android.app.Activity
-keep public class * extends androidx.appcompat.app.AppCompatActivity
# Application 클래스 보존
-keep public class * extends android.app.Application
# Service 보존
-keep public class * extends android.app.Service
# BroadcastReceiver 보존
-keep public class * extends android.content.BroadcastReceiver
# ContentProvider 보존
-keep public class * extends android.content.ContentProvider
# Fragment 보존
-keep public class * extends android.app.Fragment
-keep public class * extends androidx.fragment.app.Fragment
(3) 특수 클래스 보존
# ========== 네이티브 메서드 ==========
# JNI를 통해 호출되는 메서드는 이름 변경 금지
-keepclasseswithmembernames class * {
native <methods>;
}
# ========== Enum 클래스 ==========
# Enum의 values()와 valueOf() 메서드 보존
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# ========== Parcelable 구현체 ==========
# CREATOR 필드 보존 (Android 직렬화에 필수)
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
# ========== Serializable 구현체 ==========
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# ========== R 클래스 ==========
# 리소스 ID는 반드시 보존
-keepclassmembers class **.R$* {
public static <fields>;
}
# JNI를 통해 호출되는 메서드는 이름 변경 금지
-keepclasseswithmembernames class * {
native <methods>;
}
# ========== Enum 클래스 ==========
# Enum의 values()와 valueOf() 메서드 보존
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# ========== Parcelable 구현체 ==========
# CREATOR 필드 보존 (Android 직렬화에 필수)
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
# ========== Serializable 구현체 ==========
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# ========== R 클래스 ==========
# 리소스 ID는 반드시 보존
-keepclassmembers class **.R$* {
public static <fields>;
}
. . . . .
2) 주요 ProGuard 지시어
ProGuard 규칙 작성 시 사용되는 핵심 지시어들을 이해해야 합니다.
| 지시어 | 설명 | 사용 예시 |
|---|---|---|
| -keep | 클래스와 멤버를 그대로 유지 (축소, 최적화, 난독화 모두 방지) | -keep class com.example.User { *; } |
| -keepclassmembers | 클래스는 난독화 가능하지만 멤버는 유지 | -keepclassmembers class * { public <methods>; } |
| -keepclasseswithmembers | 특정 멤버를 포함하는 클래스 전체 유지 | -keepclasseswithmembers class * { native <methods>; } |
| -keepnames | 이름만 유지 (축소는 적용, 난독화만 방지) | -keepnames class com.example.User |
| -keepclassmembernames | 멤버 이름만 유지 | -keepclassmembernames class * { java.lang.String *; } |
| -dontwarn | 경고 무시 (미사용 라이브러리 경고 제거) | -dontwarn okio.** |
| -assumenosideeffects | 메서드 호출 제거 (로그 제거에 유용) | -assumenosideeffects class android.util.Log { *; } |
(1) 와일드카드와 필터
| 패턴 | 의미 | 예시 |
|---|---|---|
| * | 단일 레벨 와일드카드 | com.example.* (example 바로 아래 패키지만) |
| ** | 모든 하위 패키지 포함 | com.example.** (example 아래 모든 패키지) |
| *** | 모든 타입 (매개변수, 반환 타입) | *** method(***) (모든 메서드) |
| ! | 제외 (부정) | !android.** (android 패키지 제외) |
| <init> | 생성자 | public <init>() |
| <fields> | 모든 필드 | public static <fields> |
| <methods> | 모든 메서드 | native <methods> |
. . . . .
3) 실전 예제 - 자주 사용하는 패턴
(1) 모델 클래스 보존 (JSON 파싱용)
# ========== 데이터 모델 클래스 ==========
# Gson이나 Retrofit에서 사용하는 POJO 클래스
-keep class com.example.app.model.** { *; }
-keep class com.example.app.data.** { *; }
# 또는 특정 어노테이션이 있는 클래스만
-keep @com.google.gson.annotations.SerializedName class * { *; }
# Gson이나 Retrofit에서 사용하는 POJO 클래스
-keep class com.example.app.model.** { *; }
-keep class com.example.app.data.** { *; }
# 또는 특정 어노테이션이 있는 클래스만
-keep @com.google.gson.annotations.SerializedName class * { *; }
(2) 커스텀 뷰 보존
# ========== 커스텀 뷰 ==========
# XML 레이아웃에서 참조되는 커스텀 뷰
-keep public class * extends android.view.View {
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
public void set*(***); # setter 메서드 보존
}
# XML 레이아웃에서 참조되는 커스텀 뷰
-keep public class * extends android.view.View {
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
public void set*(***); # setter 메서드 보존
}
(3) WebView JavaScript 인터페이스
# ========== JavaScript 인터페이스 ==========
# @JavascriptInterface 어노테이션이 있는 메서드 보존
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# 특정 JavaScript 인터페이스 클래스
-keep class com.example.app.JavaScriptInterface {
public *;
}
# @JavascriptInterface 어노테이션이 있는 메서드 보존
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# 특정 JavaScript 인터페이스 클래스
-keep class com.example.app.JavaScriptInterface {
public *;
}
(4) 로그 제거 (릴리스 빌드에서)
# ========== 로그 제거 ==========
# 릴리스 빌드에서 모든 Log 호출 제거
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** i(...);
public static *** w(...);
public static *** e(...);
}
# 릴리스 빌드에서 모든 Log 호출 제거
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** i(...);
public static *** w(...);
public static *** e(...);
}
#4. 라이브러리별 규칙과 문제 해결
대부분의 인기 라이브러리는 특정 ProGuard 규칙이 필요합니다. 이 섹션에서는 자주 사용되는 라이브러리의 규칙과 문제 해결 방법을 다룹니다.
1) 주요 라이브러리 ProGuard 규칙
(1) Retrofit 2
# ========== Retrofit 2 ==========
-keepattributes Signature
-keepattributes Exceptions
-keep class retrofit2.** { *; }
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# Retrofit 인터페이스 유지
-keep interface retrofit2.** { *; }
-dontwarn retrofit2.**
-dontwarn okhttp3.**
-dontwarn okio.**
-keepattributes Signature
-keepattributes Exceptions
-keep class retrofit2.** { *; }
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# Retrofit 인터페이스 유지
-keep interface retrofit2.** { *; }
-dontwarn retrofit2.**
-dontwarn okhttp3.**
-dontwarn okio.**
(2) Gson
# ========== Gson ==========
-keepattributes Signature
-keepattributes *Annotation*
# Gson 클래스 유지
-keep class com.google.gson.** { *; }
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
# Gson이 사용하는 sun.misc 패키지 경고 무시
-dontwarn sun.misc.**
# Generic 타입 정보 유지
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
}
-keepattributes Signature
-keepattributes *Annotation*
# Gson 클래스 유지
-keep class com.google.gson.** { *; }
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
# Gson이 사용하는 sun.misc 패키지 경고 무시
-dontwarn sun.misc.**
# Generic 타입 정보 유지
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
}
(3) OkHttp 3
# ========== OkHttp 3 ==========
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
# OkHttp 플랫폼 사용 클래스
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
# OkHttp 플랫폼 사용 클래스
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**
(4) Glide
# ========== Glide 4 ==========
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
# Glide 생성된 API 유지
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
# Glide 생성된 API 유지
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
(5) Firebase
# ========== Firebase ==========
-keep class com.google.firebase.** { *; }
-keep class com.firebase.** { *; }
# Firebase Realtime Database
-keepattributes Signature
-keepclassmembers class com.example.app.model.** {
*;
}
# Firebase Crashlytics
-keepattributes SourceFile,LineNumberTable
-keep public class * extends java.lang.Exception
-dontwarn org.apache.**
-dontwarn org.w3c.dom.**
-keep class com.google.firebase.** { *; }
-keep class com.firebase.** { *; }
# Firebase Realtime Database
-keepattributes Signature
-keepclassmembers class com.example.app.model.** {
*;
}
# Firebase Crashlytics
-keepattributes SourceFile,LineNumberTable
-keep public class * extends java.lang.Exception
-dontwarn org.apache.**
-dontwarn org.w3c.dom.**
(6) Kotlin Serialization
# ========== Kotlin Serialization ==========
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
# Serializer 클래스 유지
-keep,includedescriptorclasses class com.example.app.**$$serializer { *; }
# Companion 객체 유지
-keepclassmembers class com.example.app.** {
*** Companion;
}
# Serializer 메서드 유지
-keepclasseswithmembers class com.example.app.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
# Serializer 클래스 유지
-keep,includedescriptorclasses class com.example.app.**$$serializer { *; }
# Companion 객체 유지
-keepclassmembers class com.example.app.** {
*** Companion;
}
# Serializer 메서드 유지
-keepclasseswithmembers class com.example.app.** {
kotlinx.serialization.KSerializer serializer(...);
}
(7) Room Database
# ========== Room Database ==========
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *
# DAO 인터페이스 유지
-keepclassmembers class * extends androidx.room.RoomDatabase {
public static ** getInstance(...);
}
-dontwarn androidx.room.paging.**
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *
# DAO 인터페이스 유지
-keepclassmembers class * extends androidx.room.RoomDatabase {
public static ** getInstance(...);
}
-dontwarn androidx.room.paging.**
. . . . .
2) 일반적인 문제와 해결 방법
(1) ClassNotFoundException 또는 NoClassDefFoundError
원인: ProGuard가 필요한 클래스를 제거하거나 난독화했습니다.
// 오류 로그 예시
java.lang.ClassNotFoundException: com.example.app.model.User
// 해결 방법
-keep class com.example.app.model.User { *; }
// 또는 패키지 전체
-keep class com.example.app.model.** { *; }
java.lang.ClassNotFoundException: com.example.app.model.User
// 해결 방법
-keep class com.example.app.model.User { *; }
// 또는 패키지 전체
-keep class com.example.app.model.** { *; }
(2) IllegalArgumentException: No enum constant
원인: Enum 클래스가 올바르게 보존되지 않았습니다.
// 해결 방법
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
(3) 리플렉션 사용 시 오류
원인: 리플렉션으로 접근하는 클래스나 메서드가 난독화되었습니다.
// Java 코드에서 리플렉션 사용
Class<?> clazz = Class.forName("com.example.app.ReflectedClass");
// 해결 방법
-keep class com.example.app.ReflectedClass { *; }
// 특정 메서드만 보존
-keepclassmembers class com.example.app.ReflectedClass {
public void methodAccessedByReflection();
}
Class<?> clazz = Class.forName("com.example.app.ReflectedClass");
// 해결 방법
-keep class com.example.app.ReflectedClass { *; }
// 특정 메서드만 보존
-keepclassmembers class com.example.app.ReflectedClass {
public void methodAccessedByReflection();
}
(4) 크래시 로그 해독하기
난독화된 앱에서 크래시가 발생하면 스택 트레이스도 난독화됩니다. mapping.txt 파일로 원래 이름을 복원할 수 있습니다.
// 난독화된 스택 트레이스 예시
at a.b.c.a.b(Unknown Source:12)
at a.b.c.d.onCreate(Unknown Source:45)
// Android Studio에서 복원하기:
1. Analyze > Analyze Stack Trace 메뉴 선택
2. 난독화된 스택 트레이스 붙여넣기
3. ProGuard mapping 파일 선택
4. 원본 클래스/메서드 이름으로 복원됨
at a.b.c.a.b(Unknown Source:12)
at a.b.c.d.onCreate(Unknown Source:45)
// Android Studio에서 복원하기:
1. Analyze > Analyze Stack Trace 메뉴 선택
2. 난독화된 스택 트레이스 붙여넣기
3. ProGuard mapping 파일 선택
4. 원본 클래스/메서드 이름으로 복원됨
. . . . .
3) 디버깅 팁
(1) 릴리스 빌드 문제 재현
ProGuard는 릴리스 빌드에만 적용되므로, 문제가 발생하면 로컬에서 릴리스 빌드를 실행해야 합니다.
// 방법 1: Android Studio에서
Build > Select Build Variant > release
Run > Run 'app'
// 방법 2: 명령줄에서
./gradlew assembleRelease
adb install app/build/outputs/apk/release/app-release.apk
Build > Select Build Variant > release
Run > Run 'app'
// 방법 2: 명령줄에서
./gradlew assembleRelease
adb install app/build/outputs/apk/release/app-release.apk
(2) usage.txt 파일 확인
usage.txt 파일을 확인하여 어떤 클래스가 제거되었는지 파악할 수 있습니다.
// usage.txt 파일 위치
app/build/outputs/mapping/release/usage.txt
// 제거된 클래스 확인
com.example.app.UnusedClass
void unusedMethod()
// 의도치 않게 제거된 경우 keep 규칙 추가
-keep class com.example.app.UnusedClass { *; }
app/build/outputs/mapping/release/usage.txt
// 제거된 클래스 확인
com.example.app.UnusedClass
void unusedMethod()
// 의도치 않게 제거된 경우 keep 규칙 추가
-keep class com.example.app.UnusedClass { *; }
(3) 점진적 디버깅
문제가 발생하면 점진적으로 클래스를 keep하면서 원인을 찾습니다.
// 1단계: 전체 패키지 보존
-keep class com.example.app.** { *; }
// 2단계: 문제가 해결되면 범위 좁히기
-keep class com.example.app.problematic.** { *; }
// 3단계: 특정 클래스만 보존
-keep class com.example.app.problematic.SpecificClass { *; }
// 4단계: 필요한 멤버만 보존
-keep class com.example.app.problematic.SpecificClass {
public <methods>;
}
-keep class com.example.app.** { *; }
// 2단계: 문제가 해결되면 범위 좁히기
-keep class com.example.app.problematic.** { *; }
// 3단계: 특정 클래스만 보존
-keep class com.example.app.problematic.SpecificClass { *; }
// 4단계: 필요한 멤버만 보존
-keep class com.example.app.problematic.SpecificClass {
public <methods>;
}
#5. 자주 묻는 질문 (FAQ)
1) Q: ProGuard를 적용하면 앱 성능이 향상되나요?
네, ProGuard는 여러 방면에서 앱 성능을 향상시킵니다. 불필요한 코드 제거로 APK 크기가 감소하여 설치 시간과 메모리 사용량이 줄어듭니다. 또한 바이트코드 최적화를 통해 메서드 호출 오버헤드가 감소하고, 실행 속도가 빨라집니다. 다만 난독화 자체는 성능에 영향을 주지 않으며, 단지 이름만 짧아질 뿐입니다. 실제 프로젝트에서는 APK 크기가 20~40% 정도 감소하는 효과를 볼 수 있습니다.
. . . . .
2) Q: ProGuard는 앱을 100% 안전하게 보호할 수 있나요?
아니요, ProGuard는 완벽한 보안 솔루션이 아닙니다. 리버스 엔지니어링을 어렵게 만들 뿐 완전히 막지는 못합니다. 숙련된 분석가는 여전히 난독화된 코드를 분석할 수 있으며, 특히 문자열이나 리소스는 난독화되지 않습니다. 따라서 중요한 알고리즘이나 API 키는 서버 측에 두거나, DexGuard 같은 상용 솔루션을 사용하거나, 네이티브 코드(C/C++)로 구현하는 추가 보안 계층이 필요합니다. ProGuard는 기본적인 보호막으로 생각하는 것이 좋습니다.
. . . . .
3) Q: 모든 빌드에 ProGuard를 적용해야 하나요?
일반적으로 릴리스 빌드에만 적용합니다. 디버그 빌드에 ProGuard를 적용하면 디버깅이 매우 어려워집니다. 브레이크포인트가 제대로 작동하지 않고, 스택 트레이스를 읽기 어려우며, 빌드 시간이 길어집니다. 다만 릴리스 전에 ProGuard가 적용된 빌드를 충분히 테스트하는 것은 매우 중요합니다. 많은 개발자들이 릴리스 직전에만 ProGuard를 켜서 예상치 못한 오류를 만나게 됩니다. 따라서 개발 후반부에는 주기적으로 릴리스 빌드로 테스트하는 것을 권장합니다.
. . . . .
4) Q: mapping.txt 파일을 왜 보관해야 하나요?
mapping.txt 파일은 버전별로 반드시 보관해야 합니다. 이 파일은 난독화된 클래스 이름(a, b, c 등)을 원래 이름(UserManager, LoginActivity 등)으로 매핑하는 정보를 담고 있습니다. 사용자로부터 크래시 리포트를 받았을 때, 해당 앱 버전의 mapping.txt 파일이 없으면 스택 트레이스를 해석할 수 없어 버그를 수정할 수 없게 됩니다. Firebase Crashlytics 같은 도구를 사용하면 자동으로 업로드되지만, 별도로도 백업해두는 것이 안전합니다. 파일 이름에 버전 코드를 포함시켜 보관하세요 (예: mapping-v1.2.3-100.txt).
. . . . .
5) Q: ProGuard 규칙을 어디서 찾을 수 있나요?
대부분의 인기 라이브러리는 공식 문서나 GitHub 저장소에 ProGuard 규칙을 제공합니다. 라이브러리의 README 파일이나 공식 문서를 먼저 확인하세요. 또한 많은 라이브러리는 AAR 파일 내부에 proguard.txt를 포함하고 있어 자동으로 적용됩니다. GitHub에서 "proguard-rules" 또는 "consumer-rules.pro"로 검색하면 유용한 규칙 모음을 찾을 수 있습니다. 추가로 Stack Overflow나 안드로이드 개발자 커뮤니티에서 특정 라이브러리의 ProGuard 규칙을 검색할 수 있습니다.
. . . . .
6) Q: R8과 ProGuard 중 어떤 것을 사용해야 하나요?
Android Studio 3.4 이상을 사용한다면 R8이 자동으로 적용되므로 별도 선택이 필요 없습니다. R8은 ProGuard보다 빠르고 효율적이며, ProGuard 규칙과 완벽히 호환됩니다. 따라서 특별한 이유가 없다면 기본 설정(R8 사용)을 유지하는 것이 좋습니다. R8은 Google에서 직접 관리하고 최적화하므로 Android 플랫폼과의 통합이 더 우수합니다. ProGuard로 되돌릴 필요는 거의 없으며, 만약 R8에서 문제가 발생한다면 ProGuard 규칙을 조정하는 것이 더 나은 해결책입니다.
. . . . .
7) Q: 라이브러리에서 -dontwarn을 남발해도 괜찮나요?
-dontwarn은 신중하게 사용해야 합니다. 이 지시어는 경고를 무시할 뿐 실제 문제를 해결하지 않습니다. 사용하지 않는 옵션 종속성(예: OkHttp의 Conscrypt)에 대한 경고는 무시해도 괜찮지만, 실제로 사용하는 클래스에 대한 경고를 무시하면 런타임 오류가 발생할 수 있습니다. 경고가 발생하면 먼저 해당 클래스가 실제로 사용되는지 확인하고, 사용된다면 적절한 -keep 규칙을 추가해야 합니다. -dontwarn은 정말로 문제없음을 확인한 후 마지막 수단으로 사용하세요.
. . . . .
8) Q: ProGuard 적용 후 빌드 시간이 너무 오래 걸립니다
ProGuard/R8 처리는 추가 시간이 필요하지만, 몇 가지 방법으로 빌드 시간을 단축할 수 있습니다. ① 디버그 빌드에서는 ProGuard를 비활성화하세요. ② -dontoptimize 옵션을 추가하면 최적화 단계를 건너뛰어 빌드 시간이 단축됩니다(보안에는 영향 없음). ③ 필요한 경우에만 -dontobfuscate로 난독화를 비활성화할 수 있습니다. ④ 최신 버전의 Android Gradle Plugin을 사용하면 R8의 성능 개선 혜택을 받을 수 있습니다. ⑤ 충분한 메모리를 할당하세요 (gradle.properties에서 org.gradle.jvmargs=-Xmx4g 설정).
. . . . .
9) Q: 난독화된 앱을 Google Play Console에 업로드하면 어떻게 되나요?
Google Play Console은 mapping.txt 파일을 자동으로 수집하여 크래시 리포트를 원본 이름으로 변환해 보여줍니다. App Bundle(.aab)로 업로드하면 매핑 파일이 자동으로 포함됩니다. APK로 업로드하는 경우 Play Console의 "앱 번들 탐색기" 섹션에서 수동으로 매핑 파일을 업로드할 수 있습니다. 업로드된 매핑 파일은 해당 버전 코드와 연결되어 저장되므로, 각 릴리스마다 올바른 매핑 파일이 연결되어 있는지 확인하세요.
. . . . .
10) Q: 특정 클래스만 난독화하고 나머지는 그대로 두고 싶습니다
기본적으로 ProGuard는 Entry Point를 제외한 모든 것을 난독화합니다. 특정 클래스를 난독화에서 제외하려면 -keep 규칙을 사용하세요. 예를 들어 공개 API나 SDK를 개발하는 경우 public 인터페이스는 유지하고 내부 구현만 난독화할 수 있습니다. -keep public class com.example.sdk.** { public *; } 규칙을 사용하면 public 멤버만 유지하고 private/protected 멤버는 난독화됩니다. 반대로 특정 클래스만 난독화하려면 기본적으로 모든 것을 keep하고 원하는 클래스만 제외하는 방식은 권장하지 않습니다.
마무리
ProGuard는 Android 앱의 보안과 성능을 향상시키는 강력하고 필수적인 도구입니다. 이번 가이드에서 다룬 내용을 요약하면 다음과 같습니다.
① ProGuard의 네 가지 핵심 기능 - 축소, 최적화, 난독화, 사전 검증
② build.gradle 설정 - minifyEnabled, shrinkResources, proguardFiles
③ 중요 파일 관리 - proguard-rules.pro 작성과 mapping.txt 보관
④ 라이브러리별 규칙 - Retrofit, Gson, OkHttp, Glide, Firebase 등
⑤ 문제 해결 - ClassNotFoundException, Enum 오류, 크래시 로그 해독
② build.gradle 설정 - minifyEnabled, shrinkResources, proguardFiles
③ 중요 파일 관리 - proguard-rules.pro 작성과 mapping.txt 보관
④ 라이브러리별 규칙 - Retrofit, Gson, OkHttp, Glide, Firebase 등
⑤ 문제 해결 - ClassNotFoundException, Enum 오류, 크래시 로그 해독
ProGuard를 처음 적용할 때는 복잡하게 느껴질 수 있지만, 올바른 규칙 설정과 충분한 테스트를 통해 안전하고 효율적인 앱을 만들 수 있습니다. 특히 mapping.txt 파일을 버전별로 보관하는 것을 잊지 마세요.
최신 Android Studio는 R8을 기본으로 사용하지만, ProGuard 규칙은 그대로 호환되므로 이 가이드의 모든 내용이 여전히 유효합니다. 여러분의 Android 앱에 ProGuard를 적용하여 더 안전하고 최적화된 앱을 만들어 보세요!
긴 글 읽어주셔서 감사합니다.
끝.
끝.
반응형
'Development > Android' 카테고리의 다른 글
| [Android] Android ViewModel과 LiveData 실전 구현 방법 (아키텍처 가이드) (0) | 2019.09.23 |
|---|---|
| [Android] Android App Architecture 완벽 가이드 - MVVM 패턴과 권장 아키텍처 설계 방법 (0) | 2019.09.23 |
| [Android] Android Button 텍스트 밑줄 추가하는 4가지 방법 (0) | 2019.09.22 |
| [Android] Android ABI 완벽 가이드 : 초보 개발자를 위한 적용 및 관리 방법 (0) | 2019.09.22 |
| [Android] Android 터치 이벤트 처리 방법: 발생 순서와 구현 완벽 분석 (0) | 2019.09.10 |