[Android] Baseline Profile 적용기 feat. Fake 주입 실패

2025. 9. 23. 20:56·Android

안녕하세요, 오늘은 UX개선 방법중 하나인 Baseline Profile에 대해서 얘기해보려고합니다. 앱 성능 최적화는 언제나 중요한 과제인것 같아요. Lazy를 사용해서 렌더링 최적화를 하고 메모리 사용을 개선하는 등등 부드러운 UX를 제공하기 위해 저도 안드로이드 개발자로서 노력하고 있습니다.

그중에서 특히 사용자가 가장 먼저 마주하는 앱 시작 속도는 전체적인 앱 경험에 큰 영향을 끼치죠. 당장 저도 로딩이 길면 화가납니다

그래서 안드로이드 진영에서 성능을 최적화 할 방법중 Baseline Profile 을 도입했어요. 오늘은 Baseline Profile을 적용하면서 있었던 일을 공유해보려 합니다ㅎㅎ

Baseline Profile이 뭔데?

Baseline Profile은 앱 설치 시 또는 백그라운드에서 자주 사용하는 코드 경로를 미리 컴파일(AOT)하여, 앱 시작 속도와 런타임 성능을 개선하는 효과적인 방법입니다. 단순히 플러그인을 추가하고 기본 템플릿만으로도 시작 프로파일(Startup Profile)을 생성할 수 있어 적용 자체는 간단해요.

사실 좀더 파고 들자면..kotlin 컴파일러로 바이트 코드를 생성해서 부터 R8 컴파일을 통해 .jar 바이트 코드를 .dex 확장자의 바이트 코드로 변환되고 앱을 스토어에서 설치할때 일어나는 AOT와 런타임에 일어나는 JIT 컴파일이 있고~ AOT는 설치과정에 AndroidOS가 이해 가능한 기계어로 변경해서 설치가 오래걸리지만 JIT는 런타임 과정에서 컴파일해서 런타임 성능이 떨어지고..등등 할 얘기는 정말 많습니다만, 이론은 공식문서에서도 알수 있는 정보이니 굳이 얘기하지는 않을게요.

그냥 현대 안드로이드는 JIT + AOT를 섞어서 사용한다고 생각하시면 됩니다.

 

기준 프로필 개요  |  App quality  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 기준 프로필 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 기준 프로필을 사용하면 포함된 코

developer.android.com

그래서 결국 Baseline Profile은 JIT 컴파일을 건너뛰게 해주는 도구 입니다. 런타임에 일어나는 컴파일이 줄어드니 앱 성능이 향상이 되겠죠? 하지만 스토어에서 생성해주는 Cloud Profile은 개발자가 컨트롤 할 수 없습니다. 그리고 업데이트마다 다시 생성되길 기다려야하죠. 때문에 저희가 Baseline Profile을 통해 일관적인 성능 개선을 제공하고자 하는거에요.

 

어떻게 적용하는데?

이것 또한 공식문서에서 꽤나 친절하게 설명해주고 있어서 크게 다루지는 않겠습니다. 간단하게 어떻게 적용했는지만 보여드릴게요.

 

기준 프로필 만들기  |  App quality  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 기준 프로필 만들기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Jetpack Macrobenchmark 라이브러리와 Bas

developer.android.com

AGP 8.2 + 부터는 안드로이드 스튜디오에서 전용 모듈 템플릿을 제공해줍니다.

위의 옵션을 프로젝트에 맞게 수정해서 Finish를 누르면 끝이에요. 그러면 이제

BaselineProfileGenerator.kt , StartupBenchmarks.kt 라는 파일이 생길거에요. 각각 Baseline Profile을 생성하는 클래스와 생성된 프로파일의 성능을 측정하는 클래스입니다. 어떻게 실행하냐구요? 두가지 방법이 있습니다.

 

1. 안드로이드 스튜디오의 Run Configuration 사용하기

보시면 명령 도구가 생기셨을거에요. 먼저 Generate Baseline Profile for app을 실행합니다. 하지만 그전에 Build Variants를 Release로 변경해주세요. 이유는 성능을 개선하려는 APK가 결국 난독화가 적용된 Release 빌드기 때문이에요. 그런데 보시면 Variants가 더있는걸 보셨을거에요.

요건 아래에서 다시 설명하겠습니다ㅎㅎ

그렇게 생성된 프로파일을 통해서 StartupBenchmarks를 실행하시면 됩니다.

 

2. Gradle Task로 실행하기

// 공식문서에서 발췌 참고: 명령줄 인터페이스에서 기준 프로필을 생성하고 설치하려면 
:app:generateBaselineProfile 또는 :app:generateVariantBaselineProfile Gradle 작업을 실행하세요.

라고 나와있듯 터미널에 입력해서 실행하시면 됩니다.

 

하지만

정말 의미 있는 성능 개선을 위해서는, 사용자가 앱에서 가장 빈번하게 상호작용하는 핵심 흐름, 즉 CUJ(Critical User Journey)를 정의하고 프로파일링해야 해요. 제가 정의한 앱의 핵심 흐름은 스플래시 → 로그인 → 홈 화면 진입 후 주요 기능 사용인데, 여기서 문제가 시작됐어요 :(

// BaselineProfileGenerator.kt
@Test
fun generateFullJourneyProfile() {
    rule.collect(packageName = targetPackage) {
        // 1. 앱 시작
        pressHome()
        startActivityAndWait()

        // 2. 로그인을 건너뛰고 홈 화면으로 가야 함
        // ???

        // 3. 홈 화면의 핵심 기능 사용
        // ...
    }
}

가장 중요한 홈 화면과 주요 기능들을 프로파일링하려면 로그인 과정을 건너뛰어야 했어요. 구글 소셜 로그인을 사용하고 있었고 테스트를 진행하기 위해 사용하는 UIAutomator는 외부 SDK의 UI를 조작하는 기능이 없기 때문이에요. 또한 화면에 진입했을때 서버와 통신해서 생기는 딜레이가 정확한 성능 측정에 방해가 되기 때문에 테스트 중에는 Fake를 주입해야해요. 저는 Hilt를 사용하고 있었기 때문에, 테스트 환경에서만 동작하는 Fake Repository를 주입하자고 생각했습니다.

그런데 안되네..

목표는 명확했어요. 테스트 환경에서만 로그인된 상태를 반환하는 FakeAuthRepository와 FakeUserRepository를 만들고, Hilt의 @TestInstallIn 어노테이션을 사용해 기존 모듈을 교체하려고 했어요. 이렇게 하면 프로덕션 코드 수정 없이 테스트 환경에서만 로그인 로직을 우회할 수 있을 거라 생각했어요. 그런데 정작 실행해보니 에러가 끊이질 않았습니다..

가장 먼저 마주한 것은 Hilt 테스트 환경을 구성하는 문제였어요. @HiltAndroidTest 어노테이션을 사용하기 위해서는 테스트 실행기(Test Runner)가 Hilt를 인지해야 했어요.

  • java.lang.IllegalStateException: Hilt test ... must use a Hilt test application but found android.app.Application.

Hilt 테스트는 일반 Application이 아닌 HiltTestApplication 환경에서 실행되어야 합니다. 그래서baselineprofile/build.gradle.kts에 Hilt가 제공하는 공식 테스트 러너를 지정해야 했어요.

  • java.lang.ClassNotFoundException: Didn't find class "dagger.hilt.android.testing.HiltTestRunner"

러너를 지정했지만, 테스트 APK를 빌드할 때 HiltTestRunner 클래스를 찾지 못하는 문제였어요. hilt-android-testing 의존성을 implementation으로 추가하여 테스트 APK에 해당 클래스가 포함되도록 수정했어요.

이처럼 여러 단계의 설정을 거쳐 Hilt 테스트 환경을 구성하는 데 성공했지만, 정작 가장 중요한 로그인 우회는 여전히 동작하지 않았어요... 테스트를 실행하면 앱은 계속해서 로그인 화면으로 진입했고, 홈 화면의 UI 요소를 찾으려던 테스트 코드는 NullPointerException을 발생시키며 실패했어요.

그래서 @UninstallModules을 사용해보려고 했어요.

각각의 데이터 모듈에서 @TestInstallIn으로 교체하는 방식이 실패했으니, 이번에는 프로파일을 생성하는 :baselineprofile 모듈에서 프로덕션 모듈을 직접 제거하고 Fake 모듈을 한 번에 주입하는 중앙 집중적인 방법을 시도해 봤어요.

// Fake 모듈을 제공하는 로컬 Hilt 모듈
@Module
@InstallIn(SingletonComponent::class)
abstract class FakeRepositoryModule {
    @Binds
    @Singleton
    abstract fun bindFakeAuthRepository(impl: FakeAuthRepository): AuthRepository
    // ... 다른 Fake Repository들도 바인딩
}

// 테스트 클래스
@HiltAndroidTest
@UninstallModules(RepositoryModule::class) // 프로덕션 모듈 제거 시도
class BaselineProfileGenerator {
    // ...
}

하지만 이 방법 역시 동일한 이유로 실패했어요. @UninstallModules 어노테이션 또한 @HiltAndroidTest 환경 하에서만 동작하는 Hilt의 테스트 기능 중 하나에요. 제가 간과했던 것처럼, generateReleaseBaselineProfile 태스크는 이 테스트 환경을 사용하지 않기 때문에 @UninstallModules 어노테이션은 무시되었고, 프로덕션 모듈은 제거되지 않았어요. 앱은 여전히 실제 RepositoryModule을 사용하여 로그인 화면으로 진입했어요.

 

이 시도를 통해, 문제는 Hilt의 특정 기능(@TestInstallIn이냐 @UninstallModules이냐)이 아니라, Baseline Profile 생성 환경과 Hilt 테스트 환경의 근본적인 차이에 있다는 것을 확실히 이해하게 되었어요.

핵심은 generateReleaseBaselineProfile Gradle 태스크의 동작 방식이었어요. 이 태스크는 :app 모듈의 nonMinifiedRelease라는, 프로덕션 코드 기반의 Build Variant을 대상으로 UI 테스트 APK를 실행시켜 프로파일을 수집해요.

 

즉, 이 과정은 androidTest 소스셋과는 완전히 분리된 환경에서 동작해요.

 

제가 각 data 모듈의 src/androidTest 폴더에 만든 FakeRepository와 @TestInstallIn 모듈들은 androidTest 환경에서 테스트를 실행할 때만 의미가 있어요. 하지만 Baseline Profile 생성 태스크는 androidTest를 실행하는 것이 아니므로, Hilt는 제가 만든 가짜 모듈의 존재 자체를 알지 못했고, 따라서 의존성 교체는 일어나지 않았어요.

결국 Hilt의 테스트 주입 기능은 제가 생각한 환경에서는 제대로 작동하지 않았어요..

아쉽지만!

커스텀 CUJ 적용은 실패했지만, 성능 개선이라는 원래 목표를 포기할 순 없었어요. 그래서 먼저 기본적으로 제공되는 앱 시작(Startup) 시나리오만이라도 적용해보기로 했습니다.

// BaselineProfileGenerator.kt
@Test
fun generateStartupProfile() {
    rule.collect(
        packageName = "com.hilingual",
        includeInStartupProfile = true // 시작 프로파일에 포함
    ) {
        pressHome()
        startActivityAndWait()
    }
}

 

별도의 UI 조작 없이 앱이 시작되고 첫 화면이 그려지기까지의 과정만 프로파일링하는 가장 간단한 형태입니다. 비록 완전한 CUJ는 아니지만, 앱의 '첫인상'을 결정하는 가장 중요한 부분의 성능을 개선할 수 있었어요.

실제로 이 기본 프로파일만 적용한 후 에뮬레이터에서 벤치마크를 실행한 결과, 중간값(median) 기준으로 약 13.2%의 시작 시간 개선을 확인할 수 있었습니다. 참고로 에뮬레이터로 측정한 벤치마크 결과는 안정성이 떨어져요. 때문에 추후에 실기기로 다시 측정을 할 예정입니다.

그런데 그러면 이상한 Build Variants는 왜 생긴건데?

Baseline Profile 플러그인을 적용하면 nonMinifiedRelease나 nonMinifiedBenchmark 라는 빌드 변형이 생긴걸 보셨을 거예요. 이걸 선택해서 Run Configuration을 실행하시면 오류가 날거에요. 그럼 왜 존재하는걸까요?

 

사실 난독화와 프로파일링 사이의 기술적 충돌을 해결하기 위한 방법이에요.

minifyEnabled = true로 설정된 릴리스 빌드는 R8에 의해 코드 난독화가 적용돼요. navigateToHome() 같은 메서드 이름이 c()처럼 의미 없는 이름으로 바뀌게 되는데요. 이 이름은 빌드할 때마다 바뀔 수 있기 때문에, 이 상태에서 생성된 프로파일 규칙(baseline-prof.txt)은 안정적이지 않고 버전 관리가 거의 불가능해요.

 

그래서 플러그인은 이 문제를 해결하기 위해 단계별로 나눠진 프로파일 단계를 진행해요.

isMinifyEnabled = false로 강제 설정된 nonMinifiedRelease 빌드를 사용해 프로파일을 생성해요. 이렇게 하면 baseline-prof.txt에는 navigateToHome()처럼 안정적이고 온전히 보전된 코드가 기록됩니다.

실제 release 빌드를 할 때, R8은 코드 난독화를 진행하며 원본 이름과 변경된 이름의 매핑 정보가 담긴 mapping.txt 파일을 생성해요. 그 후, R8은 이 mapping.txt를 참조하여 baseline-prof.txt에 있는 사람이 읽을 수 있는 규칙들을 최종 난독화된 이름(a.b.c())에 맞게 변환하여 적용해요.

 

이처럼 nonMinified 빌드 변형은 플러그인이 내부에서 프로파일을 생성하기 위한 임시 빌드 변형이에요. 그래서 ./gradlew :app:generatenonMinifiedBenchmark 를 아무리 실행해도 오류가 나게 됩니다.

이번에 Baseline Profile을 적용하면서 CUJ를 만들기 위해 3일정도 시간을 투자했는데요. 비록 Hilt를 이용한 로그인 우회는 실패했지만, 그 과정에서 Hilt 테스트 환경과 Baseline Profile 플러그인에 대해서 딥다이브하게 되어 오히려 좋다고 생각합니다. 성능 개선이라는 목표를 향한 과정이 순탄하지만은 않지만, 그 과정 속에서 얻는 배움이 더 큰 자산이 되는 것 같아요. 여러분도 Baseline Profile을 적용할 때, 저처럼 혼자서 힘들어하지 않으시길 바랍니다 😅

 

[FEAT/#413] Apply baseline profile by angryPodo · Pull Request #426 · Hi-lingual/Hilingual-Android

Related issue 🛠 closed [FEAT] Applying the baseline profile #413 Work Description ✏️ Baseline Profile Module 생성 혼재된 Build type을 Build-logic으로 리펙토링 baseline-pro.txt 생성 StartupBenchmarks_startupC...

github.com

해당 작업의 Pull Request입니다. 참고가 되시면 좋겠습니다 :)

'Android' 카테고리의 다른 글

[Android] 앱을 배포해 봅시다.  (0) 2025.10.02
[Android] 컨벤션 플러그인 뜯어고치기  (0) 2025.09.25
[Android] 실용적인 멀티모듈 아키텍처 설계 실전기 (Domain 레이어와 build-logic)  (0) 2025.08.10
'Android' 카테고리의 다른 글
  • [Android] 앱을 배포해 봅시다.
  • [Android] 컨벤션 플러그인 뜯어고치기
  • [Android] 실용적인 멀티모듈 아키텍처 설계 실전기 (Domain 레이어와 build-logic)
한민돌
한민돌
Android 개발자가 되기까지.
  • 한민돌
    미래 가젯 연구소
    한민돌
  • 전체
    오늘
    어제
    • 분류 전체보기 (20) N
      • Android (4)
        • Compose (10)
        • Jetpack (2)
      • Kotlin (2)
        • Kotlin In Action (0)
      • 외부 활동 (2) N
        • 우아한테크코스 8기 (2) N
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

    • GitHub
  • 인기 글

  • 태그

    jetpack
    custom plugin
    API35
    우테코
    coroutine
    Android
    sideeffect
    LaunchedEffect
    backing property
    AboutLibraries
    runcatching
    compose
    rememberupdatedstate
    OssLicensePlugin
    Multi-module
    producestate
    build-logic
    Baseline Profile
    Google Recommend Architecture
    derivedstateof
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
한민돌
[Android] Baseline Profile 적용기 feat. Fake 주입 실패
상단으로

티스토리툴바