Warm Start는 앱 프로세스가 이미 메모리에 존재하지만(예: 최소화 후), 리소스 절약을 위해 시스템이 Activity를 제거한 Android 앱 시작 시나리오입니다. Application.onCreate는 이미 실행되었고 클래스는 로드되었지만 UI는 새로 생성됩니다. Google, 2024에 따르면 Warm Start는 200~800ms가 소요되며 4GB RAM 장치에서 전체 시작의 약 40%를 차지합니다.
핵심 요점
Warm Start는 Cold Start와 Hot Start 사이의 상태입니다. 앱 프로세스는 메모리에 존재하지만(때로는 Linux 백그라운드 캐시에), Activity는 활성 상태가 아니며 새로 생성됩니다. Android의 RAM이 부족하면 프로세스를 살린 채 스택에서 Activity를 제거할 수 있습니다. 사용자가 앱으로 돌아가면 Warm Start가 발생합니다. 새 Activity 인스턴스가 생성되고 수명 주기 메서드 onCreate → onStart → onResume이 실행되지만 Application.onCreate와 클래스 로딩은 건너뜁니다.
Android는 프로세스 우선순위(중요도 순위)에 따라 Activity 제거를 결정합니다. 백그라운드의 Activity(수준 PROCESS_STATE_IMPORTANT_FOREGROUND 또는 PROCESS_STATE_TOP_SLEEPING)는 사용 가능한 RAM에 따라 앱 최소화 후 5~30분 내에 제거될 수 있습니다. 3GB RAM 장치에서는 10분 내에, 8GB RAM 장치에서는 몇 시간 후에 Activity가 제거될 수 있습니다. 중요한 점: Warm Start 중 onSaveInstanceState는 Activity 제거 전에 호출되며, 개발자가 UI 상태를 저장할 수 있습니다.
사용자는 Warm Start와 Cold Start의 차이를 인식하지 못합니다 — 단순히 앱 아이콘을 탭하고 기다릴 뿐입니다. 그러나 Warm Start 중 앱이 사용자 정의 시작 창 테마를 설정하지 않은 경우 흰색 화면이 나타날 수 있습니다. Google은 Warm Start 중 흰색/검은색 화면 깜빡임을 방지하기 위해 매니페스트에서 시작 Activity에 사용자 정의 테마(Theme.AppCompat.Light 또는 Theme.Material3.DayNight)를 설정할 것을 권장합니다. Android 12+에서는 SplashScreen API도 이 효과를 숨깁니다.
세 가지 시작 유형의 차이점을 이해하는 것은 올바른 프로파일링 및 최적화 전략을 선택하는 데 필수적입니다. 각 유형은 고유한 지속 시간, 병목 지점 및 측정 도구를 가지고 있습니다.
| 기준 | Cold Start | Warm Start | Hot Start |
|---|---|---|---|
| 프로세스 | 새로 생성 | 메모리에 존재 | 메모리에 존재 |
| Application.onCreate | 실행됨 | 실행되지 않음 | 실행되지 않음 |
| Activity | 처음부터 생성 | 처음부터 생성 | 스택에서 복원 |
| 시간 | 1~5초 | 200~800ms | < 200ms |
| Activity.onCreate | 전체 | 전체(복원 포함) | 건너뜀 |
실제로 Warm Start는 사용자 습관과 장치 RAM에 따라 전체 앱 시작의 30%~60%를 차지합니다. 많은 앱을 열어두는 사용자(멀티태스커)는 Warm Start를 더 자주 경험합니다. 소셜 네트워크 및 메신저의 경우 앱이 항상 백그라운드에 있으므로 Warm Start가 가장 일반적인 시나리오입니다. 반면 은행 앱의 경우 보안상의 이유로 강제 프로세스 정리로 인해 Cold Start가 우세합니다.
Warm Start는 세 단계로 구성되며, 각각 측정 및 최적화가 가능합니다. Cold Start와 달리 포크 단계나 클래스 로딩은 없지만, 비용이 많이 들 수 있는 상태 복원 단계가 있습니다.
시스템은 앱에 시작 창 테마가 있는지 확인합니다. 테마가 설정되지 않은 경우 흰색(또는 시스템에 따라 검은색) 화면이 표시됩니다. 테마가 설정된 경우 테마 배경이 표시됩니다. 이 단계는 10~30ms가 소요되지만 테마가 앱의 실제 UI와 일치하지 않으면 시각적으로 눈에 띕니다. 첫 번째 화면의 배경색과 일치하는 사용자 정의 windowBackground와 함께 Theme.Material3.DayNight를 사용하면 즉시 로딩 효과를 만들 수 있습니다.
시스템은 Activity가 제거되기 전에 onSaveInstanceState에 저장된 Bundle savedInstanceState를 전달하여 onCreate를 호출합니다. 앱이 상태(필드 텍스트, 스크롤 위치, ViewModel 데이터)를 올바르게 저장한 경우 복원이 빠르게 이루어집니다. 그렇지 않은 경우 Activity는 처음부터 시작되고 데이터가 로드되는 동안 사용자에게 로더가 표시됩니다. 핵심 포인트: ViewModel 객체는 프로세스가 제거되지 않은 경우에만 Warm Start에서 유지됩니다 — Warm Start 중 ViewModel은 메모리에 남아 있습니다.
onCreate 후 onStart → onResume이 실행되고 시스템이 첫 번째 그리기를 트리거합니다. Warm Start의 TTFD(Time To First Draw)는 중간급 장치에서 300ms 미만이어야 합니다. 첫 번째 화면에 무거운 View가 있는 복잡한 RecyclerView가 포함되어 있거나 네트워크에서 이미지를 로드하는 경우 TTFD가 임계값을 초과할 수 있습니다. 첫 번째 프레임 후 부드러운 콘텐츠 로딩을 위해 Placeholder와 Shimmer를 사용하세요.
Warm Start 측정은 Cold Start보다 복잡합니다. 프로세스는 살아 있지만 Activity는 제거된 상태를 시뮬레이션해야 하기 때문입니다. -S 플래그가 있는 표준 ADB 명령은 작동하지 않습니다 — 프로세스를 종료합니다. Warm Start에는 다른 접근 방식을 사용하세요.
먼저 adb shell monkey를 통해 앱을 시작하거나 아이콘을 탭한 다음 최소화합니다(adb shell input keyevent 3 keyevent HOME). 시스템이 Activity를 제거할 수 있도록 5~10초 기다린 후 adb shell am start -W(-S 없이)를 실행합니다. 명령은 Cold Start보다 짧은 시작 시간을 반환합니다. 재현성을 위해 스크립트를 사용하세요: 시작 → 대기 → 홈 → 대기 → 시작.
# ADB를 통한 Warm Start 시뮬레이션
$ adb shell am start -W \
com.example.app/.MainActivity
# 출력(Warm Start):
# ThisTime: 412 ms
# TotalTime: 412 ms
# WaitTime: 423 ms
androidx.benchmark.macro 라이브러리는 Warm Start 측정을 지원합니다. 테스트에서 startupMode = StartupMode.WARM을 설정하면 라이브러리가 앱을 시작하고, 최소화하고, 대기(구성 가능한 지연)한 다음 다시 시작을 측정합니다. Macrobenchmark는 10~20회 반복을 수행하고 백분위수를 계산합니다. CI/CD에서 임계값을 설정할 수 있습니다: P50 Warm Start가 600ms를 초과하면 테스트가 실패합니다. 이를 통해 커밋마다 회귀를 추적할 수 있습니다.
Firebase는 마지막 앱 종료 이후 시간을 기준으로 Cold와 Warm Start를 자동으로 구분합니다. 앱이 지난 30분 이내에 열린 경우 Firebase는 시작을 Warm으로 분류합니다. Firebase 콘솔에서 각 시작 유형에 대한 별도의 차트를 볼 수 있어 최적화 효과를 평가할 수 있습니다. 예를 들어 ViewModel에 상태 보존을 구현한 후 Warm Start 시간이 30% 감소하는 것을 확인할 수 있습니다.
Warm Start 최적화는 두 가지 영역에 초점을 맞춥니다: Activity.onCreate 속도 향상과 올바른 상태 복원. Application.onCreate와 클래스 로딩이 이미 완료되었으므로 주요 병목 지점은 첫 번째 화면의 UI 코드입니다.
저장된 상태(savedInstanceState)에 역직렬화가 필요한 데이터(Bitmap, String, JSON)가 포함된 경우 백그라운드 스레드에서 수행하세요. onCreate에서 Bundle을 직접 읽는 대신 코루틴을 시작하고 shimmer 화면을 표시합니다. 실제로 중간급 장치에서 Bundle 역직렬화는 20~100ms가 소요됩니다 — 적게 보이지만 Warm Start의 경우 전체 시간의 10~50%입니다. Jetpack의 Saved State Module을 사용하면 ViewModel 상태를 Bundle 또는 데이터베이스에 자동으로 저장하고 복원할 수 있습니다.
XML 레이아웃 인플레이션은 Warm Start에서 가장 비용이 많이 드는 단계 중 하나입니다. 첫 번째 화면이 AppBar, CollapsingToolbar, NestedScrollView 및 세 개의 RecyclerView가 있는 복잡한 CoordinatorLayout을 사용하는 경우 인플레이션 시간이 300ms에 도달할 수 있습니다. 해결책: 평평한 계층 구조를 위해 ConstraintLayout을 사용하고, 시작 시 보이지 않는 섹션(바텀 시트, 다이얼로그)에 ViewStub을 적용하며, AsyncLayoutInflater를 통해 무거운 프래그먼트의 비동기 인플레이션을 활성화합니다. Jetpack Compose에서는 인플레이션이 필요하지 않지만 Warm Start 중 Compose 트리 컴파일이 비슷한 시간이 소요될 수 있습니다.
Warm Start 중 앱이 이전 세션에서 로드한 데이터가 이미 캐시에 있을 수 있습니다: Room 데이터베이스, SharedPreferences, ViewModel의 인메모리 캐시. 첫 번째 화면이 서버의 목록을 표시하는 경우 시작 시 캐시를 확인하고 백그라운드에서 데이터를 업데이트하세요. cache-then-network 전략을 사용하세요: 먼저 캐시된 데이터를 표시하고(즉시), 그런 다음 서버에서 업데이트합니다(비동기식). 이렇게 하면 인지된 Warm Start 시간이 100~200ms로 줄어듭니다.
// Warm Start를 위한 캐싱이 있는 ViewModel
class FeedViewModel : ViewModel() {
private val cache = MutableStateFlow<List<Item>>(emptyList())
init {
// 캐시 먼저, 그다음 네트워크
viewModelScope.launch {
cache.emit(db.getItems()) // Warm Start: 데이터가 이미 DB에 있음
cache.emit(api.fetchItems()) // 백그라운드 업데이트
}
}
}
올바른 상태 보존은 좋은 Warm Start와 나쁜 Warm Start를 구분하는 핵심 요소입니다. 사용자는 앱으로 돌아와 스크롤 위치, 필드 텍스트, 선택한 탭을 포함하여 자신이 남긴 것을 정확히 보기를 기대합니다.
시스템은 Activity가 제거될 때 onSaveInstanceState를 호출하지만 프로세스가 종료되기 전입니다. Bundle에는 단순 데이터 유형(String, Int, Parcelable, Serializable)만 저장됩니다. 복잡한 데이터의 경우 ViewModel에서 SavedStateHandle을 사용하세요 — Warm Start 중 필드를 자동으로 저장하고 복원합니다. onSaveInstanceState와 달리 SavedStateHandle은 프로세스가 Warm Start에서 살아남은 경우에도 작동합니다(ViewModel이 제거되지 않음). 예: EditText의 텍스트에 SavedStateHandle.getLiveData(“text”)를 사용하면 텍스트가 자동으로 저장되고 복원됩니다.
Warm Start 중 프로세스가 종료되지 않은 경우 ViewModel은 메모리에 남아 있고 onCleared는 호출되지 않습니다. 즉, 이전 세션에서 로드된 모든 데이터를 즉시 사용할 수 있습니다. 그러나 프로세스가 종료된 경우(장치가 30분 이상 절전 모드), ViewModel이 제거되고 SavedStateHandle과 함께 새로 생성됩니다. Warm Start 중 올바른 ViewModel 동작을 위해 모든 시나리오에서 복원해야 하는 필드와 함께 SavedStateHandle을 사용하세요. 차이점: @HiltViewModel이 있는 ViewModel은 SavedStateHandle을 자동으로 지원합니다.
| 메커니즘 | 프로세스 활성 | 프로세스 종료 |
|---|---|---|
| ViewModel | 메모리에 데이터 | 제거, 새로 생성 |
| SavedStateHandle | 메모리에 데이터 | Bundle에서 복원 |
| onSaveInstanceState | Activity 제거 시 호출 | 호출되지 않음 |
| Room DB | 캐시 사용 가능 | 캐시 사용 가능(디스크) |
Warm Start의 가장 일반적인 문제 중 하나 — 스크롤 위치 손실. 사용자가 50번째 항목까지 스크롤하고, 앱을 최소화하고, 돌아왔을 때 — 목록의 처음이 보입니다. 해결책: layoutManager.onSaveInstanceState를 저장하고(첫 번째 보이는 항목의 위치와 오프셋 저장) onRestoreInstanceState에서 복원합니다. 또한 마지막 보이는 위치를 날짜/시간 키와 함께 SharedPreferences에 저장하여 Warm Start 중 빠르게 위치를 복원할 수 있습니다.
// RecyclerView 스크롤 위치 저장
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(
"rv_state", binding.recyclerView
.layoutManager?.onSaveInstanceState()
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedInstanceState?.getParcelable<Parcelable>("rv_state")
?.let { binding.recyclerView.layoutManager?.onRestoreInstanceState(it) }
}
Warm Start 최적화의 두 가지 실용적인 예: ViewModel에서 SavedStateHandle 사용 및 시작 후 복잡한 데이터의 비동기 복원.
SavedStateHandle은 Bundle에 필드를 자동으로 저장하고 Warm Start 중에 복원합니다. 사용자 프로필 필드(String, JSON)는 불필요한 서버 요청 없이 복원됩니다. 프로세스가 종료된 경우 SavedStateHandle은 Bundle에서 마지막 저장된 상태를 로드합니다.
class ProfileViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val profile: StateFlow<Profile?>
get() = savedStateHandle
.getStateFlow("profile", null)
fun loadProfile(id: String) {
viewModelScope.launch {
savedStateHandle["profile"] =
api.getProfile(id)
}
}
}
// Warm Start: profile이 null이 아님, 로더 없는 UI
// 로드 후: profile이 SavedStateHandle에서 업데이트됨
첫 번째 화면에 복잡한 레이아웃(지도, 그라데이션, 여러 목록)이 포함된 경우 AsyncLayoutInflater를 사용하여 백그라운드에서 무거운 요소를 인플레이트하세요. 레이아웃이 인플레이트되는 동안 shimmer 효과가 있는 플레이스홀더를 표시합니다. 이는 모든 밀리초가 중요한 Warm Start에서 특히 중요합니다. AsyncLayoutInflater는 백그라운드 스레드에서 실행되고 준비된 View를 메인 스레드의 콜백에 전달합니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 즉시 렌더링을 위한 Placeholder 레이아웃
setContentView(R.layout.placeholder_shimmer)
// 무거운 레이아웃의 비동기 로딩
AsyncLayoutInflater(this).inflate(
R.layout.activity_main_complex,
findViewById(R.id.container)
) { view, resId, parent ->
parent?.removeAllViews()
parent?.addView(view)
}
}
}
자주 묻는 질문
네, Warm Start 순간에 시스템이 앱 프로세스를 종료하기로 결정하면(예: 다른 앱을 위해 메모리 확보), 시작은 처음부터 Cold Start가 됩니다. 이는 여러 앱이 동시에 실행되는 2~3GB RAM 장치에서 발생합니다. 실제로 중간급 장치에서는 최소화 후 10~20분 동안만 Warm Start가 보장됩니다.
네, 프로세스가 종료되지 않은 경우 ViewModel은 메모리에 남아 있고 onCleared는 호출되지 않습니다. 이것이 Warm Start의 핵심 장점입니다: 네트워크 요청으로 로드된 모든 데이터, ViewModel의 캐시 — 모든 것이 즉시 사용 가능합니다. 프로세스가 종료된 경우 ViewModelProvider.Factory 또는 @HiltViewModel을 통해 ViewModel이 새로 생성되고 SavedStateHandle이 저장된 필드를 복원합니다.
이론적으로 Warm Start는 항상 Cold Start보다 빠르지만 실제로는 차이가 미미한 시나리오가 있습니다: Application.onCreate가 가볍고(50ms) Activity.onCreate가 무거운 경우(800ms), Warm Start(800ms)는 Cold Start(850ms)와 거의 같습니다. 이 경우 Application이 아닌 Activity.onCreate를 최적화해야 합니다 — 이것이 Warm Start의 병목 지점이 됩니다.
Android 12+의 SplashScreen API는 시작 시 즉시 시스템 스플래시(색상 배경의 아이콘)를 표시합니다 — Cold와 Warm Start 모두. Warm Start의 경우 스플래시는 100~300ms만 표시된 후 앱 UI로 대체됩니다. SplashScreen은 시작 자체를 가속화하지 않지만 Activity 생성 시간을 숨겨 인식을 개선합니다.
네, Warm Start는 Cold Start보다 2~3배 더 자주 발생하기 때문입니다. Cold Start가 1.2초, Warm Start가 600ms인 경우 시작의 40%(Warm)가 여전히 0.6초가 소요되며 이는 인지할 수 있습니다. Warm Start를 200~300ms로 최적화하면 사용자에게 즉각적인 복귀 느낌을 줍니다. 6GB 이상 RAM 장치에서 Warm Start는 전체 시작의 최대 80%를 차지할 수 있으므로 최적화가 우선 순위가 됩니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.