Android 앱 시작 속도 최적화 — SplashScreen API, SQLite 초기화, AdMob 광고 로딩 개선
Android 앱을 개발하다 보면 기능 구현에는 문제가 없지만 앱을 실행할 때 첫 화면이 늦게 표시되는 경우가 있습니다.
특히 SQLite 데이터베이스를 포함한 도감 앱이나 AdMob 광고를 사용하는 앱에서는 초기화 작업이 많아지면서 시작 속도가 느려질 수 있습니다.
대표적인 증상은 다음과 같습니다.
앱 아이콘을 눌렀는데 첫 화면이 늦게 표시됨
스플래시 화면이 지나치게 오래 유지됨
앱 시작 직후 화면이 잠깐 멈춤
SQLite 데이터베이스를 복사하는 동안 UI가 표시되지 않음
AdMob SDK 초기화가 완료될 때까지 메인 화면을 표시하지 않음
첫 화면의 RecyclerView가 늦게 나타남
앱 시작 시 검은 화면이나 빈 화면이 잠깐 표시됨
이러한 문제는 단순히 스플래시 화면을 없앤다고 해결되지 않습니다.
중요한 것은 앱 시작 시 반드시 완료해야 하는 작업과 첫 화면이 표시된 후 처리해도 되는 작업을 구분하는 것입니다.
이번 글에서는 Kotlin + XML + MVVM 기반 Android 앱에서 SplashScreen API, SQLite, Coroutine, AdMob을 활용해 앱 시작 속도를 개선하는 방법을 정리하겠습니다.
작성 기준: 2026년 10월
1. Android 앱 시작 속도가 중요한 이유
앱 시작 속도는 사용자가 앱을 실행한 뒤 첫 화면을 확인하고 실제로 조작할 수 있기까지의 시간과 관련이 있습니다.
Android에서는 앱 시작을 크게 다음과 같이 구분합니다.
| 구분 | 설명 |
|---|---|
| Cold Start | 앱 프로세스가 없는 상태에서 새로 실행 |
| Warm Start | 프로세스가 일부 유지된 상태에서 Activity 재생성 등 |
| Hot Start | 앱 프로세스와 Activity가 유지된 상태에서 다시 표시 |
일반적으로 Cold Start에서 초기화 비용이 가장 크게 발생합니다.
앱 프로세스를 생성하고 Application을 초기화한 뒤 Activity를 생성해야 하기 때문입니다.
특히 다음 작업이 메인 스레드에서 실행되면 시작 시간이 길어질 수 있습니다.
대용량 SQLite 파일 복사
복잡한 데이터베이스 조회
이미지 디코딩
광고 SDK 초기화 대기
네트워크 응답 대기
불필요한 객체 생성
동기 파일 I/O
앱 시작 최적화의 핵심은 이러한 작업을 무조건 제거하는 것이 아니라 실행 시점과 실행 스레드를 적절하게 조정하는 것입니다.
2. 먼저 앱 시작 구조 확인하기
다음과 같은 구조의 앱을 가정하겠습니다.
앱 아이콘 실행
│
▼
SplashActivity
│
├── SQLite 파일 복사
├── 광고 SDK 초기화
├── 기본 데이터 조회
└── 1초 대기
│
▼
MainActivity
│
├── RecyclerView 초기화
├── Fragment 생성
└── 데이터 표시이 구조에서는 스플래시 화면에서 모든 작업이 끝날 때까지 기다리기 쉽습니다.
하지만 대부분의 앱에서 광고 SDK 초기화나 전체 데이터 조회가 완료되어야만 첫 화면을 표시할 수 있는 것은 아닙니다.
따라서 다음과 같이 구조를 변경하는 것이 좋습니다.
앱 아이콘 실행
│
▼
Android SplashScreen
│
▼
MainActivity
│
├── 기본 UI 표시
│
├── SQLite 준비 ──────┐
│ │
├── AdMob 초기화 │
│ │
▼ ▼
첫 화면 표시 데이터 준비 완료
│
▼
RecyclerView 갱신이 구조에서는 사용자가 앱 화면을 먼저 확인할 수 있습니다.
데이터베이스 초기화나 광고 로딩은 필요한 시점에 별도로 처리합니다.
단, 첫 화면이 SQLite 데이터에 완전히 의존한다면 데이터 준비 상태를 명확하게 표시해야 합니다.
3. 기존 SplashActivity 방식의 문제점
기존 프로젝트에서는 다음과 같은 코드를 사용하는 경우가 있습니다.
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
Handler(Looper.getMainLooper()).postDelayed({
startActivity(
Intent(this, MainActivity::class.java)
)
finish()
}, 1000)
}
}이 코드는 스플래시 화면을 최소 1초 동안 유지합니다.
문제는 앱이 이미 준비되었더라도 사용자가 반드시 기다려야 한다는 것입니다.
반대로 초기화가 1초 이상 걸리는 경우에는 MainActivity가 표시된 뒤 추가 지연이 발생할 수도 있습니다.
즉, 고정된 지연 시간은 앱의 실제 준비 상태를 반영하지 못합니다.
Android 12부터는 시스템 SplashScreen API가 제공되므로 별도의 SplashActivity를 유지해야 하는 특별한 이유가 없다면 시스템 스플래시 화면을 활용하는 것이 좋습니다.
4. Android SplashScreen API 적용하기
Android 12 이상에서는 앱 시작 시 시스템 스플래시 화면이 표시됩니다.
AndroidX SplashScreen 라이브러리를 사용하면 이전 Android 버전에서도 호환되는 구현을 구성할 수 있습니다.
Gradle 의존성
app/build.gradle.kts
dependencies {
implementation(
"androidx.core:core-splashscreen:<version>"
)
}<version>은 AndroidX SplashScreen의 프로젝트 호환 안정 버전으로 변경합니다.
스플래시 테마 설정
res/values/themes.xml
<style
name="Theme.App.Starting"
parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">
@color/white
</item>
<item name="windowSplashScreenAnimatedIcon">
@drawable/ic_splash
</item>
<item name="postSplashScreenTheme">
@style/Theme.App
</item>
</style>여기서 Theme.App은 실제 앱의 기본 테마 이름으로 변경해야 합니다.
ic_splash는 스플래시 화면에 표시할 아이콘 리소스입니다.
AndroidManifest.xml 설정
<application
android:theme="@style/Theme.App">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.App.Starting">
<intent-filter>
<action
android:name="android.intent.action.MAIN" />
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>이제 MainActivity가 런처 Activity 역할을 수행합니다.
5. MainActivity에서 SplashScreen 설치하기
MainActivity에서는 installSplashScreen()을 호출합니다.
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}중요한 점은 installSplashScreen()을 super.onCreate()보다 먼저 호출해야 한다는 것입니다.
이렇게 하면 시스템 스플래시 화면에서 앱의 첫 화면으로 자연스럽게 전환할 수 있습니다.
스플래시 화면을 무조건 오래 유지하지 않기
SplashScreen API에서는 다음과 같이 유지 조건을 지정할 수 있습니다.
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition {
!isReady
}하지만 이 기능은 주의해서 사용해야 합니다.
setKeepOnScreenCondition의 조건은 메인 스레드에서 반복적으로 평가되므로 조건 내부에서 데이터베이스 조회나 파일 복사 같은 무거운 작업을 수행하면 안 됩니다.
또한 광고 로딩이나 불필요한 네트워크 요청이 완료될 때까지 스플래시를 유지하는 방식도 피하는 것이 좋습니다.
가능하면 첫 화면을 표시하고 데이터 로딩 상태를 화면 내부에서 처리하는 구조를 추천합니다.
6. SQLite 데이터베이스 초기화가 느린 이유
도감 앱이나 사전 앱에서는 SQLite 파일을 assets에 포함하는 경우가 있습니다.
예를 들어 다음과 같은 파일입니다.
app/src/main/assets/databases/catalog.db앱 최초 실행 시 이 파일을 내부 저장소로 복사해야 합니다.
문제는 파일 크기가 커질수록 복사 시간이 증가할 수 있다는 것입니다.
특히 다음과 같은 코드가 Activity의 메인 스레드에서 실행되면 화면이 멈출 수 있습니다.
copyDatabaseIfNeeded(this)SQLite 파일 복사는 디스크 I/O 작업이므로 메인 스레드에서 직접 실행하지 않는 것이 좋습니다.
7. Coroutine으로 SQLite 초기화 처리하기
SQLite 초기화 작업은 Dispatchers.IO에서 실행할 수 있습니다.
class CatalogRepository(
private val context: Context
) {
suspend fun prepareDatabase() =
withContext(Dispatchers.IO) {
copyDatabaseIfNeeded(context)
}
private fun copyDatabaseIfNeeded(context: Context) {
val dbFile =
context.getDatabasePath("catalog.db")
if (dbFile.exists()) return
dbFile.parentFile?.mkdirs()
context.assets.open(
"databases/catalog.db"
).use { input ->
dbFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
}위 코드는 Coroutine을 통한 비동기 초기화 구조를 설명하는 기본 예시입니다.
실제 서비스에서는 복사 도중 앱이 종료되었을 때 불완전한 파일이 남지 않도록 임시 파일, 무결성 검사, 파일 교체 절차를 추가해야 합니다.
또한 여러 화면에서 동시에 prepareDatabase()를 호출하지 않도록 초기화 작업을 한 곳에서 관리해야 합니다.
8. ViewModel에서 데이터베이스 준비 상태 관리하기
데이터베이스 초기화 상태를 ViewModel에서 관리하면 Activity와 Fragment에서 이를 관찰할 수 있습니다.
sealed class DatabaseState {
data object Loading : DatabaseState()
data object Ready : DatabaseState()
data class Error(
val throwable: Throwable
) : DatabaseState()
}data object는 Kotlin 버전에 따라 사용할 수 있으므로 구형 Kotlin을 사용한다면 일반 object로 변경할 수 있습니다.
ViewModel에서는 다음과 같이 처리합니다.
class MainViewModel(
private val repository: CatalogRepository
) : ViewModel() {
private val _databaseState =
MutableLiveData<DatabaseState>(
DatabaseState.Loading
)
val databaseState: LiveData<DatabaseState> =
_databaseState
fun prepareDatabase() {
viewModelScope.launch {
try {
repository.prepareDatabase()
_databaseState.value =
DatabaseState.Ready
} catch (e: Exception) {
_databaseState.value =
DatabaseState.Error(e)
}
}
}
}이제 UI에서는 데이터베이스가 준비될 때까지 로딩 상태를 표시할 수 있습니다.
viewModel.databaseState.observe(
viewLifecycleOwner
) { state ->
when (state) {
is DatabaseState.Loading -> {
showLoading()
}
is DatabaseState.Ready -> {
loadBirdList()
}
is DatabaseState.Error -> {
showError(state.throwable)
}
}
}이 코드는 Fragment 내부에서 사용하는 예시입니다.
데이터베이스 초기화에 실패한 경우에도 무한 로딩 상태로 남지 않도록 오류 처리를 구현하는 것이 중요합니다.
9. Application.onCreate()에서 무거운 작업 피하기
Android 앱에서는 Application 클래스에서 공통 초기화 작업을 수행할 수 있습니다.
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 공통 초기화
}
}하지만 Application.onCreate()는 일반적으로 앱 시작 과정의 메인 스레드에서 실행됩니다.
따라서 다음과 같은 작업을 동기적으로 수행하면 시작 속도에 영향을 줄 수 있습니다.
override fun onCreate() {
super.onCreate()
// 피해야 할 예시
copyLargeDatabase()
loadAllBirds()
initializeAllFeatures()
}특히 첫 화면에서 사용하지 않는 기능까지 모두 초기화할 필요는 없습니다.
예를 들어 설정 화면에서만 사용하는 기능은 설정 화면을 열 때 초기화하는 방식도 고려할 수 있습니다.
이러한 접근을 지연 초기화(Lazy Initialization)라고 합니다.
10. AdMob 초기화와 앱 시작 속도
AdMob을 사용하는 앱에서는 Google Mobile Ads SDK를 초기화해야 합니다.
기본적인 초기화 코드는 다음과 같습니다.
MobileAds.initialize(this) { initializationStatus ->
// 초기화 완료 콜백
}Google Mobile Ads SDK 초기화는 가능한 한 백그라운드 스레드에서 수행하도록 공식 문서에서 안내하고 있습니다.
다만 SDK 초기화 방식은 버전에 따라 달라질 수 있으므로 현재 사용하는 SDK의 공식 문서를 확인해야 합니다.
중요한 것은 AdMob SDK 초기화 완료를 앱 첫 화면 표시의 필수 조건으로 만들지 않는 것입니다.
광고가 늦게 준비되더라도 앱의 핵심 기능은 먼저 사용할 수 있어야 합니다.
11. AdMob 초기화와 광고 로딩 분리하기
다음과 같은 구조는 피하는 것이 좋습니다.
앱 실행
│
▼
AdMob 초기화
│
▼
광고 요청
│
▼
광고 로딩 완료
│
▼
메인 화면 표시광고 네트워크 응답 시간에 따라 앱 시작 시간이 달라질 수 있기 때문입니다.
대신 다음 구조를 추천합니다.
앱 실행
│
├── 메인 화면 표시
│
└── AdMob 초기화
│
▼
광고 요청
│
▼
광고 영역 표시예를 들어 Activity에서는 화면을 먼저 구성합니다.
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupNavigation()
observeViewModel()
initializeAds()
}initializeAds()는 별도의 광고 관리 객체를 통해 실행할 수 있습니다.
다만 실제 구현에서는 Google Mobile Ads SDK의 스레드 요구사항을 지켜야 합니다.
SDK 초기화, 광고 객체 생성, 광고 View 조작을 모두 임의의 백그라운드 스레드에서 실행해서는 안 됩니다.
특히 광고 View와 관련된 UI 작업은 메인 스레드에서 처리해야 합니다.
12. 광고 SDK 초기화 시 주의사항
AdMob 초기화는 단순히 MobileAds.initialize()를 호출하는 것으로 끝나지 않을 수 있습니다.
앱에 다음과 같은 기능이 있다면 초기화 순서를 고려해야 합니다.
사용자 동의 관리(UMP)
개인 맞춤 광고 설정
광고 요청 구성
테스트 광고 설정
앱 오프닝 광고(App Open Ad)
특히 사용자 동의가 필요한 지역에서는 동의 정보 처리와 광고 요청 순서를 올바르게 구성해야 합니다.
광고 로딩 속도를 개선하기 위해 개인정보 동의 절차를 생략하거나 우회해서는 안 됩니다.
또한 App Open Ad를 사용하는 경우 광고가 준비되지 않았다는 이유로 앱 시작을 무한정 지연시키지 않도록 설계해야 합니다.
13. 첫 화면 RecyclerView 초기화 최적화
앱 시작 시 RecyclerView에 대량의 데이터를 표시하는 경우에도 지연이 발생할 수 있습니다.
예를 들어 다음과 같은 코드는 주의해야 합니다.
val birds = repository.getAllBirds()
adapter.submitList(birds)데이터베이스 조회가 메인 스레드에서 실행된다면 화면이 멈출 수 있습니다.
따라서 데이터 조회는 Repository에서 비동기로 수행합니다.
suspend fun getBirds(): List<Bird> =
withContext(Dispatchers.IO) {
queryBirdsFromSQLite()
}ViewModel에서는 다음과 같이 호출합니다.
fun loadBirds() {
viewModelScope.launch {
val birds = repository.getBirds()
_birds.value = birds
}
}Fragment에서는 ListAdapter를 통해 데이터를 갱신합니다.
viewModel.birds.observe(viewLifecycleOwner) { birds ->
adapter.submitList(birds)
}이렇게 하면 데이터베이스 조회와 UI 갱신을 분리할 수 있습니다.
단, 대량의 데이터를 한 번에 메모리로 가져오는 방식이 항상 최선은 아닙니다.
데이터 규모가 매우 크다면 Paging 라이브러리나 페이지 단위 조회도 검토해야 합니다.
14. 첫 화면에서 꼭 필요한 데이터만 조회하기
앱을 시작할 때 모든 데이터를 미리 조회하는 경우가 있습니다.
예를 들어 도감 앱에서 전체 조류 데이터 10,000건을 한 번에 조회한다고 가정하겠습니다.
하지만 첫 화면에 실제로 표시되는 항목은 일부일 수 있습니다.
이 경우 초기 화면에 필요한 데이터만 조회하는 방식이 더 효율적일 수 있습니다.
SELECT
KTSN,
SpeciesKor
FROM korean_birds
ORDER BY SpeciesKor
LIMIT 50;이 코드는 최초 50개 항목을 조회하는 예시입니다.
다음 페이지를 조회할 때는 추가 데이터를 요청할 수 있습니다.
다만 LIMIT만 적용한다고 모든 상황에서 빠른 것은 아닙니다.
정렬 조건에 적절한 인덱스가 있는지, JOIN이 필요한지, 검색 조건이 무엇인지 함께 확인해야 합니다.
15. 앱 시작 속도 측정하기
앱 시작 속도는 느낌으로 판단하기보다 실제로 측정하는 것이 좋습니다.
Android에서는 adb shell am start -W 명령으로 Activity 시작 시간을 확인할 수 있습니다.
adb shell am force-stop com.example.birdapp
adb shell am start -W \
-n com.example.birdapp/.MainActivity예를 들어 다음과 같은 결과가 출력될 수 있습니다.
Status: ok
LaunchState: COLD
TotalTime: 820
WaitTime: 850
Complete위 숫자는 출력 형식을 설명하기 위한 예시이며 실제 측정 결과가 아닙니다.
TotalTime은 Activity 시작과 관련된 시간 정보를 제공합니다.
다만 이 값만으로 사용자가 실제로 콘텐츠를 보고 조작할 수 있는 시점까지 정확하게 평가할 수는 없습니다.
더 정밀한 분석에는 Android Studio Profiler, Perfetto, Macrobenchmark 등을 활용할 수 있습니다.
16. reportFullyDrawn()으로 콘텐츠 준비 시점 알리기
Android에서는 Activity가 실제로 콘텐츠를 표시할 준비가 되었음을 시스템에 알리는 reportFullyDrawn() 메서드를 제공합니다.
reportFullyDrawn()이 메서드는 초기 콘텐츠가 실제로 준비된 시점에 호출해야 합니다.
예를 들어 데이터베이스 조회가 끝나고 RecyclerView에 첫 목록이 반영된 뒤 호출하는 방식을 고려할 수 있습니다.
다만 단순히 Activity의 onCreate() 마지막에서 호출하면 실제 콘텐츠 준비 시점을 정확하게 반영하지 못할 수 있습니다.
Jetpack Activity의 FullyDrawnReporter 관련 API를 활용하는 방법도 있습니다.
17. Baseline Profile과 앱 시작 성능
앱 시작 성능을 추가로 개선하고 싶다면 Baseline Profile도 검토할 수 있습니다.
Baseline Profile은 앱의 주요 실행 경로에 대한 최적화 정보를 제공해 Android Runtime이 중요한 코드를 효율적으로 실행하도록 돕는 기술입니다.
특히 다음과 같은 작업에서 효과를 기대할 수 있습니다.
앱 Cold Start
주요 화면 진입
반복적으로 사용하는 사용자 흐름
하지만 Baseline Profile이 SQLite 파일 복사나 네트워크 응답 시간을 직접 줄여주는 것은 아닙니다.
따라서 먼저 불필요한 초기화와 메인 스레드 I/O를 제거한 뒤 추가적인 최적화 수단으로 적용하는 것이 좋습니다.
18. 실제 프로젝트에서 추천하는 시작 구조
SQLite 기반 데이터 앱과 AdMob을 사용하는 경우 다음과 같은 구조를 추천합니다.
MainActivity
│
├── installSplashScreen()
│
├── setContentView()
│
├── Navigation 초기화
│
├── ViewModel 관찰
│
├── SQLite 준비 요청
│ │
│ ▼
│ Dispatchers.IO
│ │
│ ├── DB 존재 확인
│ ├── 필요 시 DB 설치
│ └── 초기 데이터 조회
│
└── AdMob 초기화 요청
│
▼
광고 준비 후 표시첫 화면에서는 다음 상태를 구분해 표시합니다.
Loading
│
├── 데이터 준비 중
│
▼
Ready
│
└── RecyclerView 표시
Error
│
└── 재시도 버튼 표시이렇게 구성하면 데이터베이스 준비나 광고 로딩이 늦어져도 앱 전체가 멈춘 것처럼 보이는 문제를 줄일 수 있습니다.
19. 앱 시작 최적화 체크리스트
불필요한 SplashActivity 제거 검토
AndroidX SplashScreen API 적용
고정된 스플래시 지연 시간 제거
Application.onCreate()의 무거운 작업 확인
SQLite 파일 복사를 Dispatchers.IO에서 실행
SQLite 파일 복사 실패 및 무결성 검사 처리
첫 화면에 필요한 데이터만 조회
ViewModel에서 로딩·완료·오류 상태 관리
AdMob 초기화와 첫 화면 표시 분리
광고 로딩 완료를 앱 실행 조건으로 사용하지 않음
사용자 동의 처리 순서 확인
RecyclerView 데이터 조회 비동기 처리
앱 Cold Start 측정
실제 콘텐츠 준비 시점 측정
필요 시 Baseline Profile 적용 검토
20. 마무리
Android 앱 시작 속도 최적화에서 가장 중요한 것은 초기화 작업을 무조건 빠르게 실행하는 것이 아닙니다.
첫 화면을 표시하기 위해 반드시 필요한 작업과 나중에 실행해도 되는 작업을 분리하는 것이 핵심입니다.
특히 SQLite 데이터베이스와 AdMob을 사용하는 앱에서는 다음과 같은 방향으로 개선하는 것이 좋습니다.
SplashScreen API를 사용하고 불필요한 고정 지연을 제거합니다.
SQLite 복사와 조회는 메인 스레드 밖에서 처리합니다.
첫 화면에 필요한 최소한의 데이터부터 표시합니다.
AdMob 초기화와 광고 로딩을 앱의 핵심 기능과 분리합니다.
Cold Start와 실제 콘텐츠 표시 시간을 측정해 개선 효과를 확인합니다.
이러한 구조를 적용하면 앱 시작 시 발생하는 불필요한 대기 시간을 줄이고, 사용자가 더 빠르게 앱을 사용할 수 있도록 개선할 수 있습니다.
참고 자료
Android Developers — 앱 시작 시간
https://developer.android.com/topic/performance/vitals/launch-timeAndroid Developers — SplashScreen API
https://developer.android.com/develop/ui/views/launch/splash-screenAndroid Developers — SplashScreen 마이그레이션
https://developer.android.com/develop/ui/views/launch/splash-screen/migrateAndroid Developers — Kotlin Coroutines
https://developer.android.com/kotlin/coroutinesAndroid Developers — Baseline Profiles
https://developer.android.com/topic/performance/baselineprofiles/overviewGoogle AdMob — Mobile Ads SDK 시작하기
https://developers.google.com/admob/android/quick-startGoogle AdMob — UMP SDK
https://developers.google.com/admob/android/privacy
태그: Android, Kotlin, SplashScreen, SplashScreen API, 앱 시작 속도, Cold Start, SQLite, Coroutine, Dispatchers.IO, AdMob, RecyclerView, Baseline Profile, Android 성능 최적화