ViewModel은 Activity와 Fragment의 라이프사이클을 고려하여 UI 데이터를 저장하고 관리하도록 설계된 Android Jetpack Architecture 구성 요소입니다. Google I/O 2025에 따르면 Jetpack으로 구축된 최신 Android 앱의 82%에서 ViewModel이 사용됩니다. 일반 클래스와 달리 ViewModel은 화면 회전 및 기타 구성 변경을 자동으로 처리하며 데이터 손실 없이 UI 상태를 유지합니다. MVVM(Model-View-ViewModel) 아키텍처는 비즈니스 로직과 인터페이스를 연결하는 중앙 계층으로 ViewModel에 의존합니다.
핵심 사항
ViewModel은 Activity 또는 Fragment의 라이프사이클을 고려하여 사용자 인터페이스와 관련된 데이터를 저장하고 관리하도록 설계된 Android Jetpack 라이브러리의 클래스입니다. ViewModel의 주요 작업은 데이터 준비 로직을 UI 계층에서 분리하고 화면 회전, 테마 변경 또는 로케일 변경과 같은 구성 변경 중에 이 데이터를 유지하는 것입니다.
ViewModel이 등장하기 전에는 개발자가 UI 상태를 Activity나 Fragment에 직접 저장했습니다. 화면을 회전하면 Android가 Activity를 파괴하고 새로 생성하여 저장되지 않은 모든 데이터가 손실되었습니다. 해결책은 onSaveInstanceState()를 통한 상태 저장이나 onRetainNonConfigurationInstance() 사용이었지만 두 접근 방식 모두 수동 관리와 직렬화가 필요했으며 복잡한 객체에는 적합하지 않았습니다. ViewModel은 프레임워크 수준에서 이 문제를 해결합니다. 데이터는 UI와 별도로 메모리에 유지되며 Activity 재생성 시 자동으로 복원됩니다.
Android Developers 문서(2025)에 따르면 ViewModel은 프로세스의 RAM에 데이터를 저장합니다. 이는 바이트 배열로의 직렬화가 필요한 onSaveInstanceState()를 통한 Bundle 복원보다 10~50배 빠릅니다. ViewModel은 데이터가 단순한 원시 타입이나 문자열보다 복잡한 모든 화면에 권장됩니다.
ViewModel 라이프사이클은 Activity 라이프사이클과 근본적으로 다릅니다. ViewModel은 화면 회전 시 소멸되지 않으며 스코프가 완전히 종료될 때까지(Activity.finish() 또는 Fragment 제거) 유지됩니다. 즉, ViewModel에 로드된 모든 데이터는 네트워크나 데이터베이스에서 다시 로드하지 않고 구성 변경 중에도 계속 사용할 수 있습니다.
Activity 생성 시 시스템은 ViewModelProvider를 통해 ViewModel을 할당합니다. ViewModelProvider.get(ViewModel::class.java)을 처음 호출하면 새 ViewModel 인스턴스가 생성됩니다. 이후 호출(회전 후 포함)에서는 동일한 인스턴스가 반환됩니다. ViewModel 정리는 onCleared()가 호출될 때 자동으로 수행됩니다. 이 메서드는 Activity가 종료(finish())되거나 Fragment가 완전히 제거될 때 호출됩니다. 개발자는 onCleared()를 재정의하여 Flow 구독 취소, 코루틴 취소, 소켓 닫기 등의 리소스를 해제할 수 있습니다.
Google은 Jetpack 문서에서 강조합니다. ViewModel 내에 Activity나 View에 대한 참조를 절대 저장하지 마세요 — ViewModel이 UI가 있는 Activity보다 오래 유지되므로 메모리 누수가 발생합니다. 대신 LiveData, StateFlow 또는 SavedStateHandle을 사용하여 ViewModel과 UI 간에 데이터를 전달하세요.
MVVM(Model-View-ViewModel) 패턴에서 ViewModel은 View(Activity/Fragment)와 Model(리포지토리, DB, API) 사이에서 중심적인 위치를 차지합니다. View는 ViewModel(LiveData, StateFlow)의 반응형 데이터를 구독하고 변경 시 자동으로 업데이트됩니다. ViewModel은 View의 존재를 알지 못합니다. 데이터와 명령만 제공하며 View가 이를 표시하는 방법을 결정합니다.
MVP와 MVVM의 비교: MVP에서는 Presenter가 View(인터페이스)의 메서드를 직접 호출하여 강한 결합을 만듭니다. MVVM에서는 ViewModel이 반응형 데이터 스트림을 게시하고 View가 이를 구독합니다. 연결은 단방향이며 테스트 가능합니다. JetBrains Developer Survey(2024)에 따르면 68%의 Android 개발자가 MVVM을 주요 아키텍처로 사용하며 ViewModel은 이 패턴의 핵심 구성 요소입니다.
IT Sectr에서는 2018년부터 모든 상용 Kotlin 프로젝트에서 ViewModel과 함께 MVVM을 사용하고 있습니다. 실제 경험에 따르면 이 접근 방식은 책임의 명확한 분리와 에뮬레이터 없이 비즈니스 로직을 테스트할 수 있는 기능 덕분에 UI 로직 디버깅 시간을 30~40% 줄여줍니다.
ViewModelProvider는 Fragment 또는 Activity에서 ViewModel을 얻는 표준 방법입니다. 기본적으로 ViewModelProvider는 빈 생성자(인수 없음)를 통해 ViewModel을 생성합니다. ViewModel에 매개변수(예: 리포지토리 또는 애플리케이션 컨텍스트)가 필요한 경우 ViewModelProvider.Factory를 구현해야 합니다.
class UserViewModel(
private val userId: String,
private val repository: UserRepository
) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> get() = _user
fun loadUser() {
viewModelScope.launch {
_user.value = repository.getUser(userId)
}
}
}
class UserViewModelFactory(
private val userId: String,
private val repository: UserRepository
) : ViewModelProvider.Factory {
override fun create<T : ViewModel>(modelClass: Class<T>): T {
return UserViewModel(userId, repository) as T
}
}
팩토리는 Fragment 또는 Activity에서 ViewModel을 가져올 때 ViewModelProvider에 전달됩니다. SavedStateHandle은 AndroidX 1.2.0에서 도입된 대체 매개변수 전달 메커니즘입니다. ViewModel은 생성자를 통해 자동으로 SavedStateHandle을 받고 사용자 정의 팩토리를 작성하지 않고 Bundle을 통해 인수가 전달됩니다.
viewModelScope는 ViewModel에 내장되고 그 라이프사이클에 바인딩된 CoroutineScope입니다. viewModelScope에서 시작된 모든 코루틴은 onCleared()가 호출될 때 자동으로 취소되어 ViewModel 소멸 후 메모리 누수 및 백그라운드 작업을 방지합니다.
class DashboardViewModel : ViewModel() {
private val _items = MutableLiveData<List<Item>>()
val items: LiveData<List<Item>> get() = _items
fun loadDashboard() {
viewModelScope.launch(Dispatchers.IO) {
val result = repository.fetchDashboard()
withContext(Dispatchers.Main) {
_items.value = result
}
}
}
override fun onCleared() {
super.onCleared()
// 모든 viewModelScope 코루틴이 자동으로 취소됩니다
}
}
viewModelScope의 코루틴은 기본적으로 Dispatchers.Main에서 실행됩니다. 네트워크 또는 디스크 작업의 경우 withContext를 사용하여 Dispatchers.IO로 전환하거나 launch에서 디스패처를 지정합니다. Google(Android Dev Summit 2024)에 따르면 viewModelScope를 사용하면 수동 Job 관리와 비교하여 코루틴 관련 메모리 누수가 95% 감소합니다.
Hilt는 Dagger 위에 구축된 Android용 Google의 공식 의존성 주입 라이브러리입니다. Hilt를 사용하면 ViewModelProvider.Factory를 수동으로 작성할 필요가 없습니다. ViewModel 생성자에 @HiltViewModel 어노테이션을 추가하기만 하면 됩니다. Hilt가 자동으로 팩토리를 생성하고 생성자에 선언된 의존성을 주입합니다.
@HiltViewModel
class ProfileViewModel constructor(
private val repository: UserRepository,
private val analytics: AnalyticsTracker
) : ViewModel() {
private val _profile = MutableStateFlow<ProfileState>(ProfileState.Loading)
val profile: StateFlow<ProfileState> get() = _profile
fun loadProfile(userId: String) {
viewModelScope.launch {
_profile.value = ProfileState.Success(repository.getUser(userId))
analytics.logEvent("profile_loaded")
}
}
}
// Fragment에서 — 팩토리 없이:
val viewModel: ProfileViewModel = by viewModels()
Koin은 코드 생성 없이 사용할 수 있는 대체 DI 라이브러리입니다. Koin에서 ViewModel은 viewModel { }을 통해 모듈에 선언되고 Fragment에서는 by viewModel()을 통해 가져옵니다. Hilt와 Koin 사이의 선택은 프로젝트에 따라 다릅니다. Hilt는 컴파일 타임에 의존성 그래프 검증을 제공하고 Koin은 더 가볍고 kapt/ksp가 필요하지 않습니다. IT Sectr에서는 대규모 프로젝트(50개 이상 화면)에서 Hilt를 사용하고 중간 규모 프로젝트에서는 Koin을 사용합니다.
화면 회전 시 재설정되지 않는 정수 카운터를 저장하는 간단한 ViewModel입니다. MutableLiveData 및 LiveData 사용의 기본 패턴을 보여줍니다.
class CounterViewModel : ViewModel() {
private val _count = MutableLiveData(0)
val count: LiveData<Int> get() = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
fun reset() {
_count.value = 0
}
}
프로세스가 시스템에 의해 종료되더라도 상태를 자동으로 유지하기 위해 SavedStateHandle을 사용하는 ViewModel입니다. SavedStateHandle은 앱이 백그라운드에서 최소화되고 종료될 때 데이터를 저장하는 유일한 메커니즘입니다.
class FormViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val userName = savedStateHandle.getLiveData<String>("userName", "")
val email = savedStateHandle.getLiveData<String>("email", "")
fun saveName(name: String) {
savedStateHandle["userName"] = name
}
fun saveEmail(email: String) {
savedStateHandle["email"] = email
}
}
SavedStateHandle의 LiveData는 마지막 값을 Bundle에 자동으로 저장합니다. 프로세스 재생성 시(예: 앱 최소화 및 종료 후) Bundle이 복원되고 LiveData가 이전 값을 받습니다. Google 테스트에 따르면 SavedStateHandle은 Bundle에 최대 5KB의 데이터 저장을 보장합니다. 이는 텍스트 필드, ID 및 직렬화된 JSON 객체에 충분합니다.
자주 묻는 질문
ViewModel은 프로세스의 RAM에 데이터를 저장하므로 직렬화 없이 즉시 사용할 수 있으며 복잡한 객체(목록, Bitmap, 네트워크 응답)에 적합합니다. onSaveInstanceState()는 데이터를 Bundle에 직렬화하고(Android 12부터 트랜잭션당 최대 1MB) 단순한 원시 타입, String 및 Serializable/Parcelable에만 적합합니다. ViewModel + SavedStateHandle은 Google 권장 조합입니다. ViewModel은 런타임 데이터용, SavedStateHandle은 프로세스 종료 시 복원용입니다.
아니요, 시스템이 스코프 종료 시 자동으로 onCleared()를 호출합니다. viewModelStore.clear()를 통한 수동 정리는 테스트 케이스 간 누수를 방지하기 위해 테스트에서만 필요합니다. 프로덕션 코드에서는 수동으로 clear()를 절대 호출하지 마세요 — ViewModel 라이프사이클이 손상되어 예측할 수 없는 UI 동작이 발생할 수 있습니다.
네, ViewModel은 Jetpack Compose에서 viewModel() 함수를 통해 완전히 지원됩니다. Compose에서 ViewModel은 Composable 스코프 수준에서 가져오며 스코프를 벗어나면 자동으로 정리됩니다. MVVM의 Compose 버전을 단방향 데이터 흐름(UDF)이라고 합니다. ViewModel이 StateFlow를 게시하고 Composable 함수가 collectAsState()를 통해 구독합니다. 리듀서 접근 방식의 Compose 변형은 ViewModel을 사용한 MVI입니다.
Activity, Fragment, View, Context(Application 제외)에 대한 참조를 저장하는 것은 금지되어 있습니다. ViewModel이 UI 컨텍스트보다 오래 유지되므로 메모리 누수가 발생합니다. 직렬화된 View 상태(예: RecyclerView 위치)는 저장하지 마세요 — LayoutManager.onSaveInstanceState()를 사용하세요. 대량의 데이터(10MB 이상) 저장은 피하세요 — 프로세스 최소화 시 SavedStateHandle 없이 데이터가 손실됩니다.
ViewModel은 에뮬레이터 없이 일반 Kotlin 클래스처럼 테스트됩니다. 인스턴스를 만들고 메서드를 호출하고 LiveData 또는 StateFlow의 상태를 확인합니다. 코루틴 테스트를 위해 TestDispatcher와 함께 kotlinx-coroutines-test의 runTest를 사용합니다. Hilt를 사용하는 ViewModel의 경우 테스트 Fragment에서 @HiltViewModelTest 및 hiltViewModel()을 사용합니다. Google에 따르면 단위 테스트는 계측 테스트 없이 ViewModel 로직의 80~90%를 커버합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.