본문 바로가기

Android/Tech

BaselineProfile 을 활용하여 앱의 Start-Up-Time 단축하기

Unsplash, Andrew JP.

동기

재직 중인 회사에는 모 공공기관 프로젝트 수행을 위해 구매했던 전동 출입문이 하나 있습니다. 최근 회사가 이전하면서 '전동 출입문에 태블릿이 달려있으니, 활용해서 근태 관리를 해보자'는 의견이 있어 관련하여 개발에 착수했는데요. 전체 개발이 완료되었고 스토어 배포까지 모두 마쳤습니다. 실제로 모두들 잘 사용하고 있고요. 

 

앱 Start-Up-Time 에 문제가 있거나 하진 않습니다만, 전동 출입문이 하나 밖에 없다 보니, 한 번에 여러 명이 몰려있으면 약간의 병목이 생겼습니다. '큰 문제는 아니지만 그래도 조금이라도 더 빠르면 좋지 않을까?' 라는 생각이 들어, 과거에 우연히 알게 되었던(그러나 시도하지는 않았던) Baseline Profile 을 이용한 앱 Start-Up-Time 개선에 도전해보고 싶은 마음이 생겼습니다.


Baseline Profile 설정하기

 

Android Studio Flamingo 버전부터 Baseline Profile 설정을 위한 모듈을 손쉽게 추가할 수 있습니다. File - New - NewModule 을 통해 모듈을 추가하면 됩니다.

 

모듈 추가가 완료되면 BaselineProfileGenerator 파일과 StartupBenchmarks 파일이 생성됩니다. 전자는 실제로 유즈케이스를 추가하여 동작하는 코드이고, 후자는 명칭 그대로 성능 테스트를 위해 사용됩니다. 먼저, BaselineProfileGenerator 파일부터 살펴봅니다.

import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
@LargeTest
class BaselineProfileGenerator {

    @get:Rule
    val rule = BaselineProfileRule()

    @Test
    fun generate() {
        rule.collect(
            packageName = InstrumentationRegistry.getArguments().getString("targetAppId") ?: throw Exception("targetAppId not passed as instrumentation runner arg"),
            includeInStartupProfile = true
        ) {
            pressHome()
            startActivityAndWait()
        }
    }
}

 

BaselineProfile 의 실질적 구동을 위한 코드이므로 다소 직관적으로 작성할 수 있도록 되어 있습니다. 특이한 점은, 테스트 코드를 작성하는 형태로 코드를 작성해야 한다는 점인데요. AOT 컴파일 단계에 포함시키고 싶은 유저 플로우에 대한 코드를 작성하면 됩니다. 저는 주요 유저 플로우에 대한 코드를 아래와 같이 작성했습니다.

rule.collect(
    packageName = targetAppId,
    includeInStartupProfile = true
) {
    grantPermissions(packageName)
    pressHome()
    
    // 1. 앱을 실행합니다.
    startActivityAndWait()

    // 2. 앱이 켜지고 입력 창이 보일 때까지 대기합니다.
    device.wait(Until.hasObject(By.res(packageName, "edit_text_serial")), 5000)

    // 시리얼 넘버 입력창을 찾아 값을 입력합니다. (리소스 ID나 텍스트로 탐색)
    val serialInput = device.findObject(By.res(packageName, "edit_text_serial"))
    serialInput?.text = "12345"

    // 자동 로그인 체크박스를 클릭합니다.
    val autoLoginCheck = device.findObject(By.res(packageName, "checkbox_auto_login"))
    autoLoginCheck?.click()

    // 로그인 또는 진입 버튼을 클릭합니다.
    val loginBtn = device.findObject(By.text("확인"))
    loginBtn?.click()

    // 3. 메인 화면(VISITOR 문구가 있는 곳)이 뜰 때까지 대기합니다.
    device.wait(Until.hasObject(By.text("VISITOR")), 10000)

    // --- 이후 기존의 비즈니스 로직 Flow를 그대로 수행합니다 ---
    val card = device.findObject(By.text("VISITOR"))
    card?.click()
    
    device.wait(Until.hasObject(By.text("잔여시간")), 5000)
    // ... 생략
}

 

테스트 코드는 아시다시피 앱 APK 나 번들에 포함되지 않는 코드입니다. 즉, BaselineProfileGenerator 파일에 작성된 코드 자체는 앱 동작에 영향을 주지 않습니다. 그러므로 저는 AI 의 도움을 받아 빠르고 간편하게 작성했습니다. 실제 프로덕트에 포함이 되지 않는 코드이다보니, AI 를 활용함에 있어서 거칠 것이 없다는 점도 좋은 부분입니다. 이후 실질적인 적용을 위해서는 아래 명령어를 실행하여야 합니다.

./gradlew :app:generateBaselineProfile

 

BaselineProfileGenerator 코드에 작성된 테스트 케이스를 자동으로 수행합니다. 이 때 ART 는 해당 시나리오를 수행하면서 앱 내부의 어떤 클래스와 메서드들이 호출됐는지를 추적하고 기록합니다. 그 결과로 baseline-prof.txt 파일이 생성되는데요, ART 가 앱을 설치하면서 번들에 동봉된 해당 파일을 사용해서 미리 기계어로 컴파일되어 설치됩니다.


BaselineProfile 은 어떻게 Start-Up-Time 을 개선하는가?

ART 의 발전 과정에서 Start-Up-Time 의 최적화는 컴파일러 아키텍처의 구조적 한계를 극복하는 방향으로 발전해 왔습니다. Nougat 이후 ART 는 설치 시간과 저장 공간의 효율성을 위해 JIT(Just-In-Time, 때에 맞추어) 컴파일러와 AOT(Ahead-Of-Time, 미리) 컴파일러를 혼합한 하이브리드 방식을 채택했습니다.

 

이 아키텍처는 매우 효율적이긴 하지만, 앱을 처음 구동할 때 필수적인 코드들을 JIT 컴파일러가 런타임에 실시간으로 기계어로 번역해야 하는 병목 현상을 유발했습니다. 즉, 완전한 AOT 컴파일이 제공하는 즉각적인 실행 속도의 이점을 초기 상태에서는 누릴 수 없어, 런타임 오버헤드가 고스란히 Cold-Start-Up-Time 의 지연으로 직결되는 문제가 존재했습니다.

 

이러한 런타임 병목을 설치 시점에 원천 차단하는 핵심 기술이 바로 Baseline Profile 입니다. 개발 환경에서 사전에 구성된 Baseline Profile 은 앱의 초기 실행과 핵심 렌더링 경로에 관여하는 클래스와 메서드의 정확한 호출 명세를 담은 프로파일링 데이터입니다.

 

사용자가 마켓에서 앱을 디바이스에 설치하는 즉시, 안드로이드 OS의 내장 컴파일러(dex2oat)는 이 프로필을 참조하여 필수적인 코드 경로만을 선별적으로 AOT 컴파일합니다. 결과적으로 앱이 최초로 실행되는 순간, JIT 컴파일러가 코드를 해석하며 CPU 자원을 소모할 필요 없이 이미 기계어로 컴파일된 바이너리를 즉각적으로 실행하게 되어 Start-Up-Time이 비약적으로 단축됩니다.

 

여기에 빌드 타임에 개입하는 Startup Profile 이 더해지면 디스크 I/O 레벨의 최적화까지 달성하게 됩니다. Baseline Profile 이 실행 시점의 컴파일 오버헤드를 제거한다면, Startup Profile 은 R8 컴파일러와 연계하여 물리적인 DEX 메모리 레이아웃을 재배치합니다.

 

앱 구동에 필요한 수많은 파편화된 코드들을 하나의 연속된 메모리 블록(DEX 파일의 전면부)으로 군집화함으로써, OS가 메모리 페이지를 읽어 들일 때 발생하는 Page Fault 와 탐색 시간을 최소화합니다. 요컨대, Baseline Profile 의 AOT 컴파일 강제화와 Startup Profile 의 DEX 레이아웃 최적화가 상호 결합함으로써, 안드로이드 앱은 시스템 자원의 낭비 없이 극대화된 Start-Up 성능을 확보할 수 있습니다.


StartupBenchmarks 를 활용한 벤치마킹 결과

 

모듈 추가 시 함께 생성되는 StartupBenchmarks 파일에 생성된 코드로 즉시 전후 비교를 수행할 수 있습니다. 제 경우, 적용 전후의 중앙 값으로 비교 시 약 13%, 적용 전의 최대값과 적용 후의 최소값 비교 시에는 약 17% 의 Start-Up-Time 개선이 있었습니다. 다양한 유저 플로우를 사전에 등록하여 쾌적한 유저 경험을 전달할 수 있는 간단하고도 효율적인 방법입니다. 


후기

적용하기도 편하고 실제로 성능 개선도 꽤 크게 볼 수 있는 기술입니다. 원천적으로 앱 번들에 포함되지 않는 테스트 코드를 통한 적용이 가능하기 때문에, 실제 사용자 환경에 악영향을 주지 않는 기술이라는 점 또한 매력적입니다.

 

AI 툴을 활용해서 관련 코드를 매우 쉽게 작성할 수 있고, 해당 코드의 Side-Effect 가 적은 편이기 때문에, 현재 시점에는 사실 상 러닝커브가 없는 수준에 가깝습니다. 부담 없이 적용해보시는 것을 추천드립니다. 관련 내용은 아래 공식 문서에 보다 자세히 기술되어 있으니 참고하시면 좋을 것 같습니다.

긴 글 읽어주셔서 감사합니다.

 

 

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

기준 프로필을 사용하여 Android 앱 성능을 개선합니다. AOT 컴파일을 사용하여 시작 시간을 최적화하고 버벅거림을 줄입니다.

developer.android.com

 

 

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

Android 스튜디오와 Jetpack Macrobenchmark를 사용하여 기준 프로필을 생성하여 앱 시작 및 런타임 성능을 개선합니다.

developer.android.com