본문 바로가기
Learning (이해하며 정리한 것들)/기술 메모

[Android] APK는 되는데 AAB(Play Store)에서 .so 실행이 안 될 때 - useLegacyPackaging

by Finding Dory 2026. 2. 8.

[Android] APK는 되는데 AAB(Play Store)에서 .so 실행이 안 될 때

jniLibs.useLegacyPackaging = true 를 설정하지 않아 겪은 삽질기

TL;DR
APK로 빌드하면 정상 동작하는데, AAB로 빌드해서 Play Store에 올리면 동작하지 않나요?

Native library(.so)를 shell에서 실행 파일로 직접 실행하는 경우,
build.gradle.kts에 아래 설정을 반드시 추가하세요.
android {
    packaging {
        jniLibs {
            useLegacyPackaging = true
        }
    }
}

배경

제가 개발하는 앱은 ADB shell 권한으로 별도 프로세스를 실행하는 구조입니다. 앱에 포함된 libstarter.so를 ADB shell 명령으로 직접 실행하고, 이 바이너리가 내부적으로 app_process를 호출하여 서비스 프로세스를 띄웁니다.

개발 중에는 APK 빌드(assembleRelease)로 테스트했고, 모든 기능이 정상 동작했습니다. 그런데 AAB 빌드(bundleRelease)로 Google Play Store에 업로드한 뒤, 실제 기기에서 다운로드 받아 사용하니 보조 서비스가 시작되지 않는 문제가 발생했습니다.


증상
문제 상황
1APK(assembleRelease)로 빌드 → 설치 → 정상 동작
2AAB(bundleRelease)로 빌드 → Play Store 업로드 → 설치 → 서비스 시작 실패
3동일한 AAB를 bundletool로 로컬 설치 → 동일하게 실패

처음에는 R8/ProGuard 난독화 문제를 의심했습니다. 하지만 APK와 AAB 모두 동일한 R8 규칙으로 빌드되므로, R8이 원인이 아니었습니다.


원인 분석

APK와 AAB의 설치 방식 차이

구분APK (assembleRelease)AAB (Play Store)
설치 파일단일 app-release.apkbase.apk + split_config.arm64_v8a.apk + split_config.ko.apk
Native 라이브러리단일 APK에 포함split_config.{abi}.apk에 분리
.so 파일 추출파일시스템에 추출됨추출되지 않을 수 있음

AAB로 빌드하면 Google Play가 기기에 맞는 Split APK으로 분할하여 배포합니다. 이때 native library는 split_config.arm64_v8a.apk 등 별도의 APK로 분리됩니다.

실제로 기기에서 확인한 결과:

단일 APK 설치 시 — lib 디렉토리

$ ls lib/arm64/
libadb.so  libconscrypt_jni.so  libstarter.so  ...

AAB(Split APK) 설치 시 — lib 디렉토리

$ ls lib/arm64/
(비어 있음)
핵심 원인

AGP(Android Gradle Plugin)의 기본값이 변경되면서, AAB 빌드 시 native library를 파일시스템에 추출하지 않고 APK 내에서 직접 메모리 매핑으로 로드하는 방식이 기본값이 되었습니다.

System.loadLibrary()로 로드하는 일반적인 경우에는 문제가 없지만, .so 파일을 shell에서 직접 실행해야 하는 경우에는 파일이 물리적으로 존재하지 않아 실행할 수 없습니다.

실행 흐름 비교

APK 설치 (정상):

ADB 연결 shell 명령 전송 libstarter.so 실행 (파일 존재) app_process 시작

AAB 설치 (실패):

ADB 연결 shell 명령 전송 libstarter.so 실행 실패 (파일 없음)

해결 방법

build.gradle.ktsandroid 블록에 다음을 추가합니다:

android {
    // ... 기존 설정 ...

    packaging {
        jniLibs {
            useLegacyPackaging = true
        }
    }
}
이 설정의 효과

useLegacyPackaging = true로 설정하면, AAB/Split APK 설치 시에도 native library를 파일시스템에 물리적으로 추출합니다.
이렇게 하면 .so 파일을 shell에서 직접 실행할 수 있습니다.
참고: AndroidManifest.xml과의 관계

android:extractNativeLibs="true"를 AndroidManifest.xml에 설정해도, AGP가 AAB 빌드 시 이를 무시할 수 있습니다. 빌드 로그에 아래 경고가 나타난다면 useLegacyPackaging 설정이 필요합니다:
PackagingOptions.jniLibs.useLegacyPackaging should be set to true
because android:extractNativeLibs is set to "true" in AndroidManifest.xml.

이 설정이 필요한 경우
사용 방식useLegacyPackaging 필요?
System.loadLibrary("name")으로 로드불필요 (기본값으로 동작)
dlopen()으로 동적 로드불필요 (기본값으로 동작)
shell에서 .so 파일을 직접 실행 (exec/execvp)필수
app_process의 CLASSPATH로 APK 경로 전달상황에 따라 필요

대표적으로 아래와 같은 프로젝트에서 이 이슈가 발생할 수 있습니다:

1Shizuku 계열 앱 — app_process로 별도 서비스를 실행하는 구조
2Root/ADB 유틸리티 — native 바이너리를 shell에서 직접 실행
3터미널 에뮬레이터 — 번들된 바이너리를 실행 파일로 사용

삽질 기록

저는 이 원인을 찾기까지 아래 과정을 거쳤습니다. 같은 실수를 하지 않으시길 바랍니다.

1Play Store에서 앱 업데이트 → 서비스 시작 안 됨
2Debug APK로 빌드 → 정상 동작 → "R8 문제인가?"
3R8/ProGuard 규칙을 하나씩 수정하며 AAB 빌드 반복 → 전부 실패
4Release APK(assembleRelease)로 빌드 → 정상 동작
5"APK는 되고 AAB만 안 된다?" → Split APK 차이점 조사
6기기에서 lib 디렉토리 비교 → AAB 설치 시 .so 파일이 추출되지 않음을 확인
7useLegacyPackaging = true 추가 → 해결
교훈

"Debug에서 되고 Release에서 안 된다"고 해서 무조건 R8/ProGuard를 의심하지 마세요.
APK vs AAB의 패키징 차이가 원인일 수 있습니다.

문제를 좁힐 때는 Release APK를 먼저 테스트하여 R8 문제인지, 패키징 문제인지를 구분하는 것이 중요합니다.

마치며

AGP의 기본값 변경은 대부분의 앱에는 문제가 되지 않습니다. 하지만 native 바이너리를 실행 파일로 직접 사용하는 특수한 구조에서는 치명적인 이슈가 됩니다.

이 글이 저와 비슷한 구조의 앱을 개발하시는 분들께 도움이 되었으면 합니다. 특히 Shizuku 기반 앱이나, app_process를 활용하는 프로젝트를 진행 중이라면 useLegacyPackaging = true 설정을 꼭 확인해 보세요.

Android AAB Split APK useLegacyPackaging jniLibs native library app_process Shizuku Gradle