p.s 이 글에서 R8과 R8 컴파일러는 같은 것을 지칭하며, D8과 R8은 친구지만 서로 다른 개념입니다.
코틀린 코드는 코틀린 컴파일러를 통해 바이트코드로 컴파일되며, 이 바이트 코드를 R8 컴파일러가 최적화하고 D8 컴파일러가 DEX 파일로 변환한다.
이후 앱 실행시에 AOT나 JIT 컴파일을 통해 네이티브 코드로 변환되어 실행되는 원리이다.

Proguard나 R8은 개발자가 설정 파일 외에는 추가적으로 설정해야하는 것들이 적어서 Proguard나 R8의 차이점이나 변경점을 모르는 경우가 많다.
실 개발 단계에서도 안드로이드 개발자들은 각 라이브러리나 Kotlin 프로젝트에 필요한 Proguard 설정을 제외하고는 구체적으로 알아볼 필요성을 느끼지 못한다. 많은 라이브러리들이 정상적인 동작을 위해, 난독화 대상에서 제외되어야 할 요소들이 포함된 기본적인 Proguard 설정 코드를 공식 문서에서 제공해주는 덕분이다.
예시) Retrofit2의 Proguard 설정 파일 - Github
retrofit/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro at trunk · square/retrofit
A type-safe HTTP client for Android and the JVM. Contribute to square/retrofit development by creating an account on GitHub.
github.com
안드로이드에서는 AGP 3.4.0 버전 이상을 사용할 경우에는 Proguard가 아니라 R8을 사용해서 컴파일 타임 작업을 처리하기 때문에, R8을 대상으로 작성한다. 참고로 내가 현재 진행중인 사이드 프로젝트의 AGP 버전은 8.13.0이다.
오늘은 이 R8이 비단 난독화뿐만이 아니라 앱 전반의 용량 감소와 성능 향상을 위해 진행하는 최적화 과정에 대해 알아보고자 한다.
1. 안드로이드는 난독화가 필요하다
우선, 안드로이드에서 난독화가 필요한 이유부터 알아봐야 R8에 대해 더 깊은 이해가 가능하다.
경력 1년이 막 넘은 주니어 개발자 시절, 마켓에 배포한 우리 프로덕트를 누가 베끼면 어떡하지? 라는 귀엽고 어이없는 걱정을 한 적이 있다. 그래서 그 때 난독화와 리버스 엔지니어링에 대해 알아본 적이 있었다.
✨ 리버스 엔지니어링(역공학 설계)
리버싱이라고도 하며, 소프트웨어 개발 프로세스 각 단계의 산출물을 역으로 도출해내는 과정이다.
배포된 소스코드로부터 소프트웨어 설계서와 요구사항 명세서를 얻을 수 있다.
대부분의 소프트웨어는 대부분 바이너리 형태로 배포되기 때문에, 배포물의 리버싱이 쉽지 않다.
하지만 안드로이드는 Java 기반의 코드를 Dex 파일로 변환하기 때문에, Java의 취약점을 가지고 있어 Java용으로 개발된 리버싱 툴에 의해 쉽게 리버싱될 수 있다.

JVM 위에서 동작하는 바이트 코드는 하드웨어 의존성을 가지지 않는 높은 이식성을 가지지만, 그로 인해 리버싱에 취약하다는 특징을 가진다.
그래서 안드로이드에서는 SDK에 Proguard(R8)을 포함시켜 리버싱을 방해할 수 있는 방법을 기본적으로 제공하며, 이를 통해 난독화된 코드는 보안 취약점을 찾아내는 과정을 어렵게 만든다는 장점도 존재한다.
2. R8의 역할과 동작 원리
앱이 최대한 적은 용량을 차지하면서도 빠르게 동작하게 만들려면 빌드를 최적화하고 축소시키는 과정이 필요하다. 그 과정에서 R8은 앱 축소, 난독화/최적화를 담당하여 앱 크기를 줄이고 성능을 개선하는 최적화를 수행한다.
✨ 안드로이드의 R8 컴파일러
R8은 Proguard과의 호환성을 고려해서 개발된 안드로이드 빌드 프로세스에 최적화된 툴이며, 기존의 Proguard 규칙을 그대로 적용할 수 있다. 빌드 과정에서 R8은 D8(Dex 컴파일러)와 통합되어 Dex 파일 생성, 최적화, 난독화를 동시에 수행하게 된다.
Proguard보다 개선된 최적화 알고리즘을 사용해서 APK의 크기를 더 효과적으로 줄이고, 빌드 시간이 단축된다는 장점을 가지고 있다.
✨ R8 컴파일러의 역할
(1) 코드 축소
코드 축소는 앱이나 라이브러리 코드에서 사용하지 않는 클래스/필드/메소드 등을 찾아 삭제하는 과정이다.

위 그림처럼 앱이 어떤 라이브러리의 기능 중 일부만 사용하는 경우, R8 컴파일러는 이외의 코드들에 대해 "도달할 수 없음"으로 간주하고 컴파일 과정에서 삭제한다.
여기서 우리가 Proguard 설정 파일을 커스텀하는 이유가 나오는데, 만약 앱이 런타임에 리플렉션 API를 사용해서 코드를 탐색하는 경우에 이미 R8 컴파일러가 해당 코드를 "도달할 수 없음"이라 판단하여 해당 코드를 컴파일 타임에 삭제해버렸다면 문제가 발생하기 때문이다.
그래서 우리는 이런 상황을 방지하기 위해 -keep 규칙을 추가하거나, 클래스 어노테이션으로 @Keep을 추가하여 컴파일 타임에 R8이 해당 코드를 삭제하는 것을 방지하는 것이다.
(2) 리소스 축소
리소스 축소는 코드 축소와 함께 사용해야만 동작하는 기능이다. 정확하게는 R8이 수행하는 최적화는 아니고, AGP가 수행하지만 R8의 코드 축소와 연동되어 진행된다.
코드 축소 과정에서 사용하지 않는 코드를 모두 삭제하면, 남아있는 "사용하는 코드" 중 앱에 사용되는 리소스를 식별할 수 있다.
만약 사용하지 않는 코드를 삭제하지 않으면 해당 코드들이 참조하는 리소스들을 리소스 축소기가 삭제할 수 없기 때문이다. (한마디로, 사용하는 코드들이 참조하는 리소스만 남기기 위해서)
그래서 minifyEnabled를 사용해서 앱을 빌드하지 않은 상태라면 shrinkResources를 사용하도록 설정하기 전에 먼저 빌드를 해야하는 것이다.(언젠지는 기억나지 않지만 예전에 이것 때문에 헤멘 적이 있었는데, 이제 이유를 알았다ㅠㅠ)
android {
...
buildTypes {
getByName("release") {
isShrinkResources = true
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro"
)
}
}
}
그리고 간혹 다국어를 지원하는 앱의 경우에는 en, fr, iw, es 등 국가 코드가 포함된 대체 리소스 폴더가 존재한다.
리소스 축소기가 원본 리소스를 삭제하는 경우에도 이런 대체 리소스는 삭제하지 않기 때문에, 별도의 설정을 해줘야 특정 언어의 리소스들에 대해서도 축소할 수 있다.
전 직장에서 진행했던 프로젝트가 그랬다. 20+@개의 다국어를 빌드에 따라 선택적으로 적용해야 했는데, 모델에 따라 영어 + @ 조합으로 2-3개 정도의 언어를 지원했다.
아마 그 프로젝트에도 빌드 최적화를 위해 아래와 같은 설정이 되어있었을 것이다.(당시에는 신입이라 잘 몰랐다 ㅠㅠ)
android {
defaultConfig {
...
resourceConfigurations.addAll(listOf("en", "fr"))
}
}
(3) 코드 최적화
예외 처리 관련 포스팅에서 try-catch 문을 사용할 때 경우에 따라 컴파일러의 최적화가 제한될 수 있다는 점을 알게 되었는데, 그 최적화를 R8 컴파일러가 수행한다.
코드 최적화를 위해 R8 컴파일러는 사용하지 않는 코드를 삭제하거나, 간소화가 가능할 경우에는 코드를 간결하게 다시 작성한다.
아래는 최적화의 몇 가지 예시이다.
✨ if/else 문에서 else 분기를 사용하지 않을 경우, R8은 else 분기 코드를 삭제할 수 있다
✨ 코드의 호출부가 적을 경우, R8은 이 메서드를 삭제하는 대신 호출부에서 인라인으로 처리할 수 있다
✨ 어떤 클래스의 서브 클래스가 1개만 존재하고, 클래스 자체가 인스턴스화되지 않는다면, R8은 두 클래스를 결합하여 앱에서 클래스를 삭제할 수 있다
이건 추상 클래스와 구현 클래스의 관계가 1:1일 때를 말한다. R8은 추상 클래스가 직접 인스턴스화 되지 않으면 두 클래스를 병합해서 클래스 정의를 제거함으로써 APK 크기를 줄일 수 있다.
이게 가장 와닿았던 최적화 기법인데 그 이유는,
클린 아키텍처를 적용하다 보면 핵심적인 원칙인 "변동성이 작은 것에 의존하라"를 따르기 위해 많은 클래스들이 1:1로 추상화 되는데, 이 때 생기는 "구조적으로 중요하지만 런타임에서는 불필요한" 추상화 계층을 R8이 컴파일 타임 최적화를 통해 제거하고 있었다는 것을 알 수 있다.
(4) 코드 난독화
아래 코드는 난독화 결과물의 예시 코드이다.
androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
android.content.Context mContext -> a
int mListItemLayout -> O
int mViewSpacingRight -> l
android.widget.Button mButtonNeutral -> w
int mMultiChoiceItemLayout -> M
boolean mShowTitle -> P
int mViewSpacingLeft -> j
int mButtonPanelSideLayout -> K
난독화는 클래스/필드/메소드 명을 단축해서 앱의 크기를 줄이는 과정이다.
난독화 자체로 앱의 코드를 삭제하지는 않지만, 여러 클래스/필드/메서드가 포함된 DEX 파일을 사용하는 경우에는 앱의 크기를 크게 줄일 수 있다.
난독화를 사용하는 경우 또한 "리플렉션 사용"으로 인한 문제점을 예방하기 위해서 Keep 규칙을 설정해야 하며,
이를 통해 원래 이름을 유지하여 런타임에 해당 클래스/필드/메소드의 정보에 접근할 수 있게 된다.
3. Apk와 App bundle
[래퍼런스]
박희완,박희광,고광만,최광훈,윤종희(2012). 안드로이드를 위한 난독화 도구 프로가드(Proguard) 성능 평가, 제37회 한국정보처리학회 춘계학술대회 논문집 제19권 1호
'개발 > Android' 카테고리의 다른 글
[안드로이드] Context와 메모리 누수 (0) | 2025.04.07 |
---|---|
[안드로이드] Hilt 어노테이션 프로세싱 - Dagger를 휘두르는 방법 (0) | 2025.03.25 |
[안드로이드] Vector Drawable 변환 과정과 Bitmap(feat.dp를 사용하면 모든 화면에서 동일하게 보일까?) (0) | 2025.03.06 |
[안드로이드] 쉘 스크립트로 이미지를 dpi 폴더에 분류하기(feat.해상도 대응) (0) | 2025.03.04 |
[안드로이드] ART의 GC는 어떻게 동작할까(feat.Dalvik) (0) | 2025.02.17 |
p.s 이 글에서 R8과 R8 컴파일러는 같은 것을 지칭하며, D8과 R8은 친구지만 서로 다른 개념입니다.
코틀린 코드는 코틀린 컴파일러를 통해 바이트코드로 컴파일되며, 이 바이트 코드를 R8 컴파일러가 최적화하고 D8 컴파일러가 DEX 파일로 변환한다.
이후 앱 실행시에 AOT나 JIT 컴파일을 통해 네이티브 코드로 변환되어 실행되는 원리이다.

Proguard나 R8은 개발자가 설정 파일 외에는 추가적으로 설정해야하는 것들이 적어서 Proguard나 R8의 차이점이나 변경점을 모르는 경우가 많다.
실 개발 단계에서도 안드로이드 개발자들은 각 라이브러리나 Kotlin 프로젝트에 필요한 Proguard 설정을 제외하고는 구체적으로 알아볼 필요성을 느끼지 못한다. 많은 라이브러리들이 정상적인 동작을 위해, 난독화 대상에서 제외되어야 할 요소들이 포함된 기본적인 Proguard 설정 코드를 공식 문서에서 제공해주는 덕분이다.
예시) Retrofit2의 Proguard 설정 파일 - Github
retrofit/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro at trunk · square/retrofit
A type-safe HTTP client for Android and the JVM. Contribute to square/retrofit development by creating an account on GitHub.
github.com
안드로이드에서는 AGP 3.4.0 버전 이상을 사용할 경우에는 Proguard가 아니라 R8을 사용해서 컴파일 타임 작업을 처리하기 때문에, R8을 대상으로 작성한다. 참고로 내가 현재 진행중인 사이드 프로젝트의 AGP 버전은 8.13.0이다.
오늘은 이 R8이 비단 난독화뿐만이 아니라 앱 전반의 용량 감소와 성능 향상을 위해 진행하는 최적화 과정에 대해 알아보고자 한다.
1. 안드로이드는 난독화가 필요하다
우선, 안드로이드에서 난독화가 필요한 이유부터 알아봐야 R8에 대해 더 깊은 이해가 가능하다.
경력 1년이 막 넘은 주니어 개발자 시절, 마켓에 배포한 우리 프로덕트를 누가 베끼면 어떡하지? 라는 귀엽고 어이없는 걱정을 한 적이 있다. 그래서 그 때 난독화와 리버스 엔지니어링에 대해 알아본 적이 있었다.
✨ 리버스 엔지니어링(역공학 설계)
리버싱이라고도 하며, 소프트웨어 개발 프로세스 각 단계의 산출물을 역으로 도출해내는 과정이다.
배포된 소스코드로부터 소프트웨어 설계서와 요구사항 명세서를 얻을 수 있다.
대부분의 소프트웨어는 대부분 바이너리 형태로 배포되기 때문에, 배포물의 리버싱이 쉽지 않다.
하지만 안드로이드는 Java 기반의 코드를 Dex 파일로 변환하기 때문에, Java의 취약점을 가지고 있어 Java용으로 개발된 리버싱 툴에 의해 쉽게 리버싱될 수 있다.

JVM 위에서 동작하는 바이트 코드는 하드웨어 의존성을 가지지 않는 높은 이식성을 가지지만, 그로 인해 리버싱에 취약하다는 특징을 가진다.
그래서 안드로이드에서는 SDK에 Proguard(R8)을 포함시켜 리버싱을 방해할 수 있는 방법을 기본적으로 제공하며, 이를 통해 난독화된 코드는 보안 취약점을 찾아내는 과정을 어렵게 만든다는 장점도 존재한다.
2. R8의 역할과 동작 원리
앱이 최대한 적은 용량을 차지하면서도 빠르게 동작하게 만들려면 빌드를 최적화하고 축소시키는 과정이 필요하다. 그 과정에서 R8은 앱 축소, 난독화/최적화를 담당하여 앱 크기를 줄이고 성능을 개선하는 최적화를 수행한다.
✨ 안드로이드의 R8 컴파일러
R8은 Proguard과의 호환성을 고려해서 개발된 안드로이드 빌드 프로세스에 최적화된 툴이며, 기존의 Proguard 규칙을 그대로 적용할 수 있다. 빌드 과정에서 R8은 D8(Dex 컴파일러)와 통합되어 Dex 파일 생성, 최적화, 난독화를 동시에 수행하게 된다.
Proguard보다 개선된 최적화 알고리즘을 사용해서 APK의 크기를 더 효과적으로 줄이고, 빌드 시간이 단축된다는 장점을 가지고 있다.
✨ R8 컴파일러의 역할
(1) 코드 축소
코드 축소는 앱이나 라이브러리 코드에서 사용하지 않는 클래스/필드/메소드 등을 찾아 삭제하는 과정이다.

위 그림처럼 앱이 어떤 라이브러리의 기능 중 일부만 사용하는 경우, R8 컴파일러는 이외의 코드들에 대해 "도달할 수 없음"으로 간주하고 컴파일 과정에서 삭제한다.
여기서 우리가 Proguard 설정 파일을 커스텀하는 이유가 나오는데, 만약 앱이 런타임에 리플렉션 API를 사용해서 코드를 탐색하는 경우에 이미 R8 컴파일러가 해당 코드를 "도달할 수 없음"이라 판단하여 해당 코드를 컴파일 타임에 삭제해버렸다면 문제가 발생하기 때문이다.
그래서 우리는 이런 상황을 방지하기 위해 -keep 규칙을 추가하거나, 클래스 어노테이션으로 @Keep을 추가하여 컴파일 타임에 R8이 해당 코드를 삭제하는 것을 방지하는 것이다.
(2) 리소스 축소
리소스 축소는 코드 축소와 함께 사용해야만 동작하는 기능이다. 정확하게는 R8이 수행하는 최적화는 아니고, AGP가 수행하지만 R8의 코드 축소와 연동되어 진행된다.
코드 축소 과정에서 사용하지 않는 코드를 모두 삭제하면, 남아있는 "사용하는 코드" 중 앱에 사용되는 리소스를 식별할 수 있다.
만약 사용하지 않는 코드를 삭제하지 않으면 해당 코드들이 참조하는 리소스들을 리소스 축소기가 삭제할 수 없기 때문이다. (한마디로, 사용하는 코드들이 참조하는 리소스만 남기기 위해서)
그래서 minifyEnabled를 사용해서 앱을 빌드하지 않은 상태라면 shrinkResources를 사용하도록 설정하기 전에 먼저 빌드를 해야하는 것이다.(언젠지는 기억나지 않지만 예전에 이것 때문에 헤멘 적이 있었는데, 이제 이유를 알았다ㅠㅠ)
android {
...
buildTypes {
getByName("release") {
isShrinkResources = true
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro"
)
}
}
}
그리고 간혹 다국어를 지원하는 앱의 경우에는 en, fr, iw, es 등 국가 코드가 포함된 대체 리소스 폴더가 존재한다.
리소스 축소기가 원본 리소스를 삭제하는 경우에도 이런 대체 리소스는 삭제하지 않기 때문에, 별도의 설정을 해줘야 특정 언어의 리소스들에 대해서도 축소할 수 있다.
전 직장에서 진행했던 프로젝트가 그랬다. 20+@개의 다국어를 빌드에 따라 선택적으로 적용해야 했는데, 모델에 따라 영어 + @ 조합으로 2-3개 정도의 언어를 지원했다.
아마 그 프로젝트에도 빌드 최적화를 위해 아래와 같은 설정이 되어있었을 것이다.(당시에는 신입이라 잘 몰랐다 ㅠㅠ)
android {
defaultConfig {
...
resourceConfigurations.addAll(listOf("en", "fr"))
}
}
(3) 코드 최적화
예외 처리 관련 포스팅에서 try-catch 문을 사용할 때 경우에 따라 컴파일러의 최적화가 제한될 수 있다는 점을 알게 되었는데, 그 최적화를 R8 컴파일러가 수행한다.
코드 최적화를 위해 R8 컴파일러는 사용하지 않는 코드를 삭제하거나, 간소화가 가능할 경우에는 코드를 간결하게 다시 작성한다.
아래는 최적화의 몇 가지 예시이다.
✨ if/else 문에서 else 분기를 사용하지 않을 경우, R8은 else 분기 코드를 삭제할 수 있다
✨ 코드의 호출부가 적을 경우, R8은 이 메서드를 삭제하는 대신 호출부에서 인라인으로 처리할 수 있다
✨ 어떤 클래스의 서브 클래스가 1개만 존재하고, 클래스 자체가 인스턴스화되지 않는다면, R8은 두 클래스를 결합하여 앱에서 클래스를 삭제할 수 있다
이건 추상 클래스와 구현 클래스의 관계가 1:1일 때를 말한다. R8은 추상 클래스가 직접 인스턴스화 되지 않으면 두 클래스를 병합해서 클래스 정의를 제거함으로써 APK 크기를 줄일 수 있다.
이게 가장 와닿았던 최적화 기법인데 그 이유는,
클린 아키텍처를 적용하다 보면 핵심적인 원칙인 "변동성이 작은 것에 의존하라"를 따르기 위해 많은 클래스들이 1:1로 추상화 되는데, 이 때 생기는 "구조적으로 중요하지만 런타임에서는 불필요한" 추상화 계층을 R8이 컴파일 타임 최적화를 통해 제거하고 있었다는 것을 알 수 있다.
(4) 코드 난독화
아래 코드는 난독화 결과물의 예시 코드이다.
androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
android.content.Context mContext -> a
int mListItemLayout -> O
int mViewSpacingRight -> l
android.widget.Button mButtonNeutral -> w
int mMultiChoiceItemLayout -> M
boolean mShowTitle -> P
int mViewSpacingLeft -> j
int mButtonPanelSideLayout -> K
난독화는 클래스/필드/메소드 명을 단축해서 앱의 크기를 줄이는 과정이다.
난독화 자체로 앱의 코드를 삭제하지는 않지만, 여러 클래스/필드/메서드가 포함된 DEX 파일을 사용하는 경우에는 앱의 크기를 크게 줄일 수 있다.
난독화를 사용하는 경우 또한 "리플렉션 사용"으로 인한 문제점을 예방하기 위해서 Keep 규칙을 설정해야 하며,
이를 통해 원래 이름을 유지하여 런타임에 해당 클래스/필드/메소드의 정보에 접근할 수 있게 된다.
3. Apk와 App bundle
[래퍼런스]
박희완,박희광,고광만,최광훈,윤종희(2012). 안드로이드를 위한 난독화 도구 프로가드(Proguard) 성능 평가, 제37회 한국정보처리학회 춘계학술대회 논문집 제19권 1호
'개발 > Android' 카테고리의 다른 글
[안드로이드] Context와 메모리 누수 (0) | 2025.04.07 |
---|---|
[안드로이드] Hilt 어노테이션 프로세싱 - Dagger를 휘두르는 방법 (0) | 2025.03.25 |
[안드로이드] Vector Drawable 변환 과정과 Bitmap(feat.dp를 사용하면 모든 화면에서 동일하게 보일까?) (0) | 2025.03.06 |
[안드로이드] 쉘 스크립트로 이미지를 dpi 폴더에 분류하기(feat.해상도 대응) (0) | 2025.03.04 |
[안드로이드] ART의 GC는 어떻게 동작할까(feat.Dalvik) (0) | 2025.02.17 |