Google Play Console에 앱 번들을 올릴 때마다 마주했던 경고 메시지가 있다. 바로 소스코드를 난독화하라는 것이다. 도대체 난독화가 뭐길래 구글에서 권장하고 있는 것일까? 그리고 어떻게 할 수 있을까?
난독화(Obfuscation)
코드 난독화는 프로그래밍 언어로 작성된 코드에 대해 읽기 어렵게 만드는 작업이다. apk 파일을 디컴파일(decompile)하면 앱의 소스코드를 확인할 수 있다. 이를 이용해 코드를 임의로 수정한 후 빌드까지 하게 된다면 보안에 위협을 줄 수 있는 변조 앱을 만들 수 있는 것이다.
실제로 jadx와 같은 디컴파일 툴을 사용하면 아래와 같이 소스코드를 열어 볼 수 있다.
DEX
DEX(Dalvik Executable)는 Android 운영 체제에서 실행되는 바이트 코드 형식이다.
안드로이드에서 Kotlin 코드는 다음과 같은 순서로 실행된다.
- Kotlin 소스 코드는 Kotlin 컴파일러인
kotlinc
에 의해 JVM 바이트 코드로 컴파일된다.
- Proguard나 R8등의 도구는 이 JVM 바이트 코드를 최적화한다.
- 최적화된 JVM 바이트 코드는 DEX 형식으로 변환된다.
- DEX 파일이 Android에서 Runtime Execution (런타임 실행)된다.
65K 메서드 제한 문제
DEX(Dalvik Executable) 파일은 16비트 참조 구조로 인해 메서드를 최대 65,536개까지만 담을 수 있다. 이는 DEX 파일 자체의 기술적 한계에서 비롯된 제약사항이다. 라이브러리 의존성이 늘어나고 앱 규모가 커질수록, 개발자들은 이른바 "65K 문제"와 자주 부딪히게 된다. 특히 서드파티 라이브러리를 적극적으로 활용하는 프로젝트에서는 이 한계치에 금방 도달하곤 한다.
이러한 제약을 극복하기 위한 해결책으로 멀티 DEX가 등장했다. 여러 개의 DEX 파일을 하나의 앱에 포함시키는 방식이다. 다만 이 방식에도 단점이 있다. 앱 실행 시 다수의 DEX 파일을 불러와야 하므로 런칭 타임이 길어질 수 있기 때문이다. Android 5.0부터는 ART(Android Runtime)가 도입되면서 상황이 나아졌다. ART는 AOT(Ahead-of-Time) 컴파일링을 지원하여, 앱 설치 단계에서 바이트코드를 네이티브 코드로 미리 변환한다. 이로 인해 멀티 DEX로 인한 성능 저하가 상당 부분 개선되었다. 하지만 여전히 멀티 DEX 사용은 앱의 용량과 실행 속도에 일정 부분 영향을 미치므로, 개발 시 이를 고려해야 한다.
이러한 문제점을 개선하고, 앱을 최적화하기 위해 Proguard나 R8과 같은 도구를 사용할 수 있다.
Proguard/R8
Android Gradle 플러그인 3.4.0 이상부터는 Proguard 대신 R8 컴파일러를 이용한다.
안드로이드에서는 코드 난독화 및 축소 등을 지원하는 라이브러리가 존재한다. Proguard와 R8 컴파일러가 대표적이다. app 단위
build.gradle
파일에 isMinifyEnabled = true
옵션을 추가하여 이 기능을 활성화할 수 있다.buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } }
proguard-rules.pro
파일에는 Proguard(R8) 컴파일 시 구성을 지정할 수 있다. 몇 가지 속성을 소개하고 넘어가겠다.keepattributes <SourceFile,LineNumberTable>
: 소스 파일의 라인을 섞지 않음(Stack trace에서 정보를 얻기 어려운 문제 해결)
keep class <PackageName>
: 난독화를 진행하지 않는다. 보통 소스코드가 공개되어 있는 라이브러리에 적용한다.
Retrofit과 같은 일부 라이브러리는 난독화 및 축소 과정을 거쳤을 때 예기치 못한 오류가 발생할 수 있기 때문에 충분한 테스트를 거쳐 적절한 구성으로 진행해야 한다.
Proguard(R8) 옵션을 활성화하면 앱 빌드시 코드 난독화 뿐만 아니라 다음과 같은 작업들을 수행한다.
- 코드 축소(또는 Tree Shaking): 앱 및 라이브러리 종속 항목에서 미사용 클래스, 필드, 메서드, 속성을 감지하여 삭제한다.
- 리소스 축소: 앱 라이브러리 종속 항목의 미사용 리소스를 포함하여 패키징된 앱에서 사용하지 않는 리소스를 삭제한다.
- 최적화: 코드를 검사하고 다시 작성하여 런타임을 개선함으로써 앱 DEX 파일*의 크기를 더 줄일 수 있다. 예를 들어 주어진 if/else 구문의
else {}
분기가 전혀 사용되지 않음을 R8에서 감지한 경우 R8이else {}
분기 코드를 삭제하는 식이다.
- 난독화: 클래스 및 멤버 이름을 단축해 코드를 DEX 파일 크기를 줄이고, 코드를 읽기 어렵게 만든다.
이렇게 난독화 및 코드 최적화를 진행하고 나면, 빌드한 apk파일을 디컴파일 툴을 이용해 열어보더라도 아래와 같이 알아볼 수 없는 형식으로 표시되는 것을 확인할 수 있다.
리소스 파일과 소스코드에 대한 축소 및 최적화도 진행되었기에 빌드된 apk 파일의 크기를 비교해보더라도 큰 폭으로 줄어드는 것을 확인할 수 있다.
Proguard나 R8을 통해 난독화 및 최적화를 진행한 앱 번들을 Play Console을 통해 배포할 때 네이티브 디버그 기호라는 것을 업로드해주는 것이 비정상 종료 분석 등에 용이하다.
위와 같이 App Bundle을 업로드하고 나면 에셋에 네이티브 디버그 기호를 업로드할 수 있는 버튼이 표시된다.
app/build/intermediates/merged_native_libs/프로젝트폴더/out/lib
하위에 있는
x86_64
, armeabi-v7a
등과 같은 여러 폴더들을 하나의 ZIP 파일로 압축하여 업로드해주면 된다.
댓글