APK와 AAB, 그리고 R8 최적화

aufcl4858·2025년 10월 19일
post-thumbnail

안드로이드 개발을 하다 보면 앱 배포와 최적화에 대한 고민이 늘 따라온다. 특히 앱 크기가 커지고 사용자들의 기대치가 높아지면서, 효율적인 배포와 최적화는 선택이 아닌 필수가 되었다. 오늘은 APK에서 AAB로의 전환, 그리고 R8 최적화까지 안드로이드 앱 배포의 진화 과정을 정리해보려고 한다.

APK - 안드로이드 앱의 전통적인 배포 방식

APK(Android Package)는 우리가 가장 친숙한 안드로이드 앱 파일 형식이다. ZIP 형식으로 압축되어 있고, 앱 실행에 필요한 모든 것이 들어있다.

APK의 구조

APK 파일을 열어보면 다음과 같은 구성 요소들을 볼 수 있다:

  • classes.dex: 컴파일된 자바/코틀린 코드
  • resources.arsc: 컴파일된 리소스 파일
  • AndroidManifest.xml: 앱의 메타 정보
  • res/: 이미지, 레이아웃 등 리소스 파일
  • lib/: 네이티브 라이브러리 (armeabi-v7a, arm64-v8a, x86 등)

APK의 문제점

하지만 APK 방식에는 근본적인 문제가 있다. 모든 디바이스를 위한 리소스를 하나의 파일에 담아야 한다는 것이다.

예를 들어, 내가 Galaxy S24를 사용하는 한국 사용자라고 해보자:

  • 필요한 것: 한국어 리소스, xxhdpi 이미지, arm64-v8a 라이브러리
  • 실제 다운로드: 100개국 언어, 모든 화면 밀도 이미지, 모든 CPU 아키텍처 라이브러리

결과적으로 필요 없는 리소스까지 다운로드하게 되어 앱 크기가 불필요하게 커진다.

AAB - 구글의 신규 가이드라인

2018년, 구글은 Android App Bundle(AAB)이라는 새로운 배포 형식을 발표했다. AAB는 APK의 한계를 극복하기 위해 설계되었다.

AAB의 핵심 개념

AAB는 "중간 형식"이다. 개발자는 AAB를 업로드하고, Google Play가 각 디바이스에 최적화된 APK를 생성해서 배포한다.

// build.gradle 설정
android {
    bundle {
        language {
            enableSplit = true  // 언어별 분할
        }
        density {
            enableSplit = true  // 화면 밀도별 분할
        }
        abi {
            enableSplit = true  // CPU 아키텍처별 분할
        }
    }
}

Dynamic Feature Modules

AAB의 또 다른 강점은 동적 기능 모듈이다. 모든 기능을 처음부터 설치할 필요 없이, 필요할 때 다운로드할 수 있다.

// 동적 모듈 요청 예시
val request = SplitInstallRequest.newBuilder()
    .addModule("premium_features")
    .build()
    
splitInstallManager.startInstall(request)

이커머스 앱에서 AR 기능처럼 일부 사용자만 쓰는 무거운 기능을 분리하면 초기 설치 크기를 크게 줄일 수 있다.

AAB 전환 시 주의사항

2021년 8월부터 Google Play Store는 신규 앱에 AAB를 의무화했다. 하지만 몇 가지 제약이 있다:

  • Google Play Store 전용 (다른 마켓에는 APK 필요)
  • 직접 설치 불가 (테스트 시 번들툴 사용 필요)
  • 150MB 크기 제한 (압축 기준)

R8 - 코드 최적화

AAB가 리소스 최적화를 담당한다면, R8은 코드 최적화를 책임진다. ProGuard의 후속으로 개발된 R8은 더 빠르고 강력한 최적화를 제공한다.

R8의 세 가지 핵심 기능

1. 코드 축소 (Shrinking)
사용하지 않는 코드를 찾아서 제거한다. 실제로 놀라울 정도로 많은 코드가 제거된다.

// 축소 전
public class UserRepository {
    public User getUser(int id) { ... }      // 사용됨
    public void deleteUser(int id) { ... }   // 미사용
    public List<User> getAllUsers() { ... }  // 미사용
    private void logAccess() { ... }         // 미사용
}

// 축소 후
public class UserRepository {
    public User getUser(int id) { ... }
}

2. 난독화 (Obfuscation)
리버스 엔지니어링을 어렵게 만든다. 특히 금융 앱이나 게임처럼 보안이 중요한 경우 필수다.

// 난독화 전
com.myapp.payment.PaymentProcessor.processPayment()

// 난독화 후  
a.b.c.a()

3. 최적화 (Optimization)

  • 메서드 인라이닝: 작은 메서드를 호출하는 대신 해당 코드를 직접 삽입하여 메서드 호출 오버헤드를 제거한다.
  • 상수 폴딩: 컴파일 시점에 계산 가능한 상수 연산을 미리 계산하여 런타임 연산을 줄인다.
  • 데드 코드 제거: 실행될 수 없는 조건문이나 도달 불가능한 코드 경로를 제거한다.
  • 클래스 병합: 단일 구현만 있는 인터페이스나 추상 클래스를 구체 클래스와 병합한다.
  • 열거형 최적화: Enum을 정수 상수로 변환하여 메모리 사용량과 메서드 수를 줄인다.
  • 문자열 최적화: 중복된 문자열 상수를 하나로 통합하고 불필요한 StringBuilder 사용을 제거한다.
  • 람다 최적화: Kotlin 람다를 가능한 경우 정적 메서드로 변환하여 객체 생성을 줄인다.

R8 적용하기

기본 설정은 간단하다:

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true  // 리소스도 함께 축소
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
                         'proguard-rules.pro'
        }
    }
}

하지만 실제로는 각 프로젝트에 맞는 규칙 설정이 중요하다:

# Retrofit 모델 클래스 유지
-keep class com.myapp.api.model.** { *; }

# Gson을 위한 필드명 유지
-keepclassmembers class * {
    @com.google.gson.annotations.SerializedName <fields>;
}

# JavaScript 인터페이스 보호
-keepclassmembers class com.myapp.WebAppInterface {
    public *;
}

마무리

APK에서 AAB로, ProGuard에서 R8로의 전환은 단순한 도구의 변화가 아니다. 사용자 경험을 개선하고 앱의 품질을 높이는 중요한 진화다.

특히 한국처럼 5G가 보편화되었다고 해도, 앱 크기 최적화는 여전히 중요하다:

  • 설치 전환율 향상 (크기가 작을수록 설치 완료율 높음)
  • 저장 공간 부족 사용자 대응
  • 업데이트 속도 개선

다음 릴리즈에 AAB와 R8을 적용해보는 건 어떨까? 처음엔 설정이 번거로울 수 있지만, 한 번 세팅하면 지속적인 이익을 가져다준다. 무엇보다 "우리 앱 용량이 너무 커요"라는 리뷰가 줄어드는 것만으로도 충분한 가치가 있다.

profile
데브누누

0개의 댓글