J.
BLOG
글 목록 구독
기술

Rust 공통 모듈을 크로스플랫폼에서 공유하기 - 5편. Android 네이티브 라이브러리 패키징

2026.07.05 · 읽기 6분

Android 앱에 Rust core를 넣는 순간, Kotlin 코드만 보던 작업은 native library packaging까지 이어진다.

UniFFI가 Kotlin binding을 만들어주더라도 실제 앱에 들어가는 것은 ABI별 .so 파일이다. 이 파일이 누락되거나, 잘못된 ABI로 들어가거나, Google Play와 Android platform 요구사항을 만족하지 못하면 앱은 빌드, 설치, 실행 중 어느 단계에서든 깨질 수 있다.

이 글은 2026-07-05 기준 Android Developers 문서를 바탕으로 Rust 공통 모듈을 Android 앱에 통합할 때 확인해야 할 packaging 기준을 정리한다. 특히 ABI별 산출물, Gradle 연결, Kotlin adapter, 16 KB page size, release artifact 검증을 한 흐름으로 본다.

Android는 ABI별 산출물이 필요하다

Android native library는 ABI별로 패키징된다. Android NDK 문서는 armeabi-v7a, arm64-v8a, x86, x86_64를 지원 ABI로 설명한다. 실제 배포 범위는 앱 정책에 따라 달라질 수 있지만, release APK나 AAB 안에 어떤 ABI가 들어가는지는 명확해야 한다.

Rust core를 Android에서 사용한다면 각 ABI target에 맞춰 .so를 만들어야 한다. 일반적인 배치 형태는 다음과 같다.

apps/android/
  app/
    src/main/
      jniLibs/
        arm64-v8a/libdomain_core.so
        armeabi-v7a/libdomain_core.so
        x86_64/libdomain_core.so

이 구조가 해결하는 것은 Android packaging system이 ABI에 맞는 native library를 찾을 수 있게 하는 일이다. 한계도 있다. 파일을 수동으로 복사하면 Rust core는 바뀌었는데 Android 앱에는 예전 .so가 들어간 상태가 쉽게 생긴다.

그래서 Rust 빌드는 Android 빌드 과정에 연결하는 편이 낫다.

preBuild
  dependsOn buildRustCoreForAndroid

buildRustCoreForAndroid
  -> ABI별 cargo build
  -> UniFFI Kotlin binding 생성
  -> jniLibs / generated source 경로에 output 배치

이 흐름은 stale artifact 문제를 줄인다. 다만 Gradle task의 inputs와 outputs를 선언하지 않으면 매번 Rust를 다시 빌드하거나, 반대로 변경을 놓치는 문제가 생길 수 있다. 로컬 개발 속도와 CI 재현성을 같이 보려면 task 경계를 명시해야 한다.

Kotlin binding은 앱 계층에 바로 퍼뜨리지 않는다

UniFFI가 생성한 Kotlin binding은 편리하다. 하지만 그 타입을 ViewModel, Composable, repository에 직접 퍼뜨리면 앱 전체가 binding 구조에 묶인다.

Android 앱에는 Kotlin adapter를 둔다.

interface SudokuEngineClient {
    fun startGame(request: StartGameRequest): GameSnapshot
    fun applyAction(snapshot: GameSnapshot, action: GameAction): Transition
}

이 interface가 해결하는 것은 앱 내부 모델과 generated binding의 결합을 줄이는 일이다. 실제 구현체는 UniFFI binding을 호출하고, ViewModel은 interface만 본다. 테스트에서는 fake implementation을 넣을 수 있다.

아직 남는 판단도 있다. Rust 호출이 CPU 비용이 크다면 main thread에서 실행하면 안 된다. ViewModel scope 안에서 적절한 dispatcher를 사용하고, 결과만 UI state로 반영하는 구조가 필요하다. Compose UI는 Rust가 있는지 몰라도 되게 만드는 편이 유지보수에 유리하다.

16 KB page size는 release checklist에 넣는다

Android native library를 포함한다면 16 KB page size 요구사항을 반드시 확인해야 한다.

Google Play는 2025년 11월 1일부터 Android 15 이상을 타깃하는 신규 앱과 기존 앱 업데이트가 64-bit 기기에서 16 KB page size를 지원해야 한다고 안내한다. 앱이 직접 NDK를 쓰지 않더라도 SDK를 통해 native library를 포함하면 영향을 받을 수 있다. Rust로 만든 .so도 예외가 아니다.

최신 NDK를 쓰면 기본값은 좋아졌다. Android 문서는 NDK r28 이상이 기본적으로 16 KB alignment로 compile한다고 설명한다. 그래도 검사는 생략하면 안 된다. release artifact에 들어간 모든 native library가 최종 기준이다.

릴리즈 전에 다음을 확인한다.

  • 앱에 포함된 모든 native library가 16 KB ELF alignment를 만족하는지
  • 16 KB page size emulator 또는 실제 기기에서 실행되는지
  • third-party SDK가 native library를 포함하는지
  • debug build가 아니라 release APK 또는 AAB 기준으로 검사했는지

특히 third-party SDK가 문제를 만들 수 있다. 우리 Rust .so는 NDK r28 이상으로 맞췄지만, 광고 SDK나 분석 SDK가 오래된 native library를 포함하면 전체 앱이 영향을 받는다.

검사 명령도 release artifact에 붙여야 한다.

zipalign -c -P 16 -v 4 app-release.apk
adb shell getconf PAGE_SIZE

첫 번째 명령은 APK alignment를 확인한다. 두 번째 명령은 테스트 기기가 실제로 어떤 page size 환경인지 확인한다. 이 두 값이 해결하는 것은 “빌드는 됐다”와 “16 KB 환경에서 실행할 수 있다” 사이의 간극이다. AAB만 만드는 프로젝트라면 Play Console pre-launch report나 bundle에서 생성한 APK 기준 검사까지 포함해야 한다.

AGP와 NDK 버전을 명시한다

2026-07-05 기준 Android Gradle plugin 9.1.1 문서는 Gradle 9.3.1, JDK 17, 기본 NDK 28.2.13676358을 호환성 표에 적고 있다. AGP 9.0 계열부터 기본 NDK가 r28 계열로 올라왔고, 9.1.1에서도 같은 NDK 기본값을 유지한다.

Rust native build를 Android 빌드에 붙일 때는 이 환경을 문서화해야 한다. 로컬 개발자마다 NDK 버전이 다르면 .so alignment나 linker behavior가 달라질 수 있다.

최소한 다음 값은 한 곳에 고정한다.

AGP version
Gradle version
JDK version
NDK version
Rust toolchain
Android Rust targets

이 목록은 새 설정 파일을 만들자는 뜻이 아니다. 이미 version catalog, Gradle wrapper, CI image, README 중 한 곳에서 관리하고 있다면 그곳을 기준으로 삼으면 된다. 중요한 것은 fresh clone에서 ./gradlew assembleRelease 또는 CI release build가 Rust core 생성까지 포함해 성공하는 것이다.

release artifact를 기준으로 검사한다

native library 문제는 debug build에서는 안 보이다가 release에서 드러날 수 있다.

R8, minify, packagingOptions, ABI split, App Bundle 생성 과정이 관여하기 때문이다. 그래서 debug APK만 보고 끝내면 부족하다.

확인 대상은 실제 배포에 가까운 artifact여야 한다.

  • release APK 또는 AAB 안에 필요한 ABI가 들어 있는지
  • native library 이름이 Kotlin binding에서 기대하는 이름과 맞는지
  • System.loadLibrary 시점에 실패하지 않는지
  • 16 KB page size 검사에서 통과하는지
  • fresh install 후 첫 Rust 호출이 성공하는지

이 검사는 가능하면 CI에 넣는 것이 좋다. 최소한 release candidate를 만들 때 수동 체크리스트로라도 남겨야 한다. Rust core는 앱 내부 구현처럼 보여도, 배포 관점에서는 native binary dependency다.

그래서 무엇부터 보면 좋을까

  • Android 앱에 포함할 ABI 범위를 정한다.
  • Rust .so를 ABI별로 생성하는 Gradle task를 만든다.
  • jniLibs에 수동 복사한 산출물이 stale해지지 않게 한다.
  • UniFFI Kotlin binding은 adapter 내부에 숨긴다.
  • Rust 호출이 무거우면 main thread에서 실행하지 않는다.
  • AGP, Gradle, JDK, NDK, Rust toolchain 버전을 문서화한다.
  • 16 KB page size 대응을 release checklist에 넣는다.
  • third-party SDK native library도 함께 검사한다.
  • debug build가 아니라 release artifact 기준으로 검증한다.

마무리

Android에서 Rust core를 쓰는 일은 Kotlin API를 하나 추가하는 정도로 끝나지 않는다. ABI, .so, NDK, Gradle task, Play 요구사항이 같이 따라온다.

특히 2026년에는 16 KB page size 대응을 가볍게 보면 안 된다. Rust native library를 넣는 순간 앱은 native packaging 검증 대상이 된다. 빌드가 되는지보다, 어떤 artifact가 만들어지고 어떤 기기 조건에서 실행되는지를 확인해야 한다.

다음 글에서는 Web에서 같은 Rust core를 WebAssembly package로 다루는 방식을 정리하겠다.

출처

광고 Coupang Partners

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

#Rust#Android#Kotlin#Gradle#NDK
이전 글 Rust 공통 모듈을 크로스플랫폼에서 공유하기 - 4편. Apple 플랫폼과 XCFramework
© 2026 진재명 · blog.jaemyeong.com iOS 소프트웨어 엔지니어 · 부산