안드로이드 아키텍처 용어 사전
MVVMMVVM

ViewModel

구성 변경을 넘어 살아남으며 UI 상태를 보유하는 컴포넌트. 화면보다 오래 산다.

구성 변경(화면 회전 등)을 넘어 살아남으며 UI 상태를 보유하는 컴포넌트.

생명주기가 화면보다 길다

회전 시

  • Activity — onDestroy → onCreate (새 인스턴스) ViewModel 그대로 유지된다

최종 종료 시 (뒤로가기 · finish()) ViewModel 의 onCleared() 가 불리고 소멸한다

"회전했더니 데이터를 다시 불러온다" 는 문제가 이것으로 사라진다.

MVVM의 세 역할

  • Model — 데이터와 비즈니스 (Repository · UseCase)
  • View — 화면 (Activity · Fragment · Composable) — 상태를 그리기만 한다
  • ViewModel — 상태를 보유하고 이벤트를 처리한다. View 를 모른다

ViewModel이 View를 모른다는 것이 핵심이다 — 그래서 테스트에서 안드로이드 없이 돌릴 수 있다.

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val repository: UserRepository,
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val _uiState = MutableStateFlow(HomeUiState())
    val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()

    fun onRefresh() = viewModelScope.launch {
        _uiState.update { it.copy(isLoading = true) }
        val users = repository.getUsers()
        _uiState.update { it.copy(isLoading = false, users = users) }
    }
}

절대 하면 안 되는 것 — Context 참조

// ❌ Activity 가 파괴돼도 ViewModel 이 참조를 들고 있다 → 메모리 누수
class MyViewModel(private val activity: Activity) : ViewModel()
class MyViewModel : ViewModel() { var view: TextView? = null }

// ✅ 필요하면 Application Context 를 쓴다 (AndroidViewModel)
// 더 나은 방법: Context 가 필요한 일을 Repository 로 내린다

SavedStateHandle — 프로세스 죽음에서 살아남기

ViewModel 은 '구성 변경' 은 넘지만 '프로세스 종료' 는 못 넘는다

  • 백그라운드에서 시스템이 앱을 죽이면 ViewModel 도 사라진다

SavedStateHandle 에 넣은 값은 그것도 넘는다 (Bundle 로 저장된다)

  • 검색어 · 선택된 탭 · 스크롤 위치처럼 작은 것만 넣는다
val query: StateFlow<String> = savedStateHandle.getStateFlow("query", "")
fun onQueryChange(q: String) { savedStateHandle["query"] = q }

면접 함정

  • "ViewModel은 모든 상황에서 데이터를 유지한다" → 프로세스 종료는 못 넘는다. SavedStateHandle이 필요하다.
  • "ViewModel에 LiveData/StateFlow만 있으면 MVVM" → View를 모르는 것이 본질이다.

viewModelScope — 코루틴이 자동으로 취소된다

viewModelScope.launch { ... }   // onCleared() 시 자동 취소

왜 중요한가

  • GlobalScope.launch 를 쓰면 화면이 사라져도 작업이 계속 돈다
  • 이미 없는 화면의 상태를 갱신하려다 참조를 붙들고 있는다 → 누수

viewModelScope 는 내부적으로 SupervisorJob + Dispatchers.Main.immediate 다

  • 자식 하나가 실패해도 형제가 죽지 않는다

상태를 어떻게 노출하나 — stateIn

val uiState: StateFlow<HomeUiState> = repository.observeUsers()
    .map { HomeUiState(users = it) }
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),   // 구독 끊기고 5초 뒤 중단
        initialValue = HomeUiState(isLoading = true)
    )

WhileSubscribed(5_000) 의 5초가 뭘 푸나 화면 회전 중에는 잠깐 구독자가 0 이 된다 즉시 중단하면 회전 직후 네트워크를 다시 탄다

  • 5초 유예를 두면 회전은 넘기고, 진짜 떠난 경우는 정리한다

어느 ViewModel 인스턴스를 받나

  • by viewModels() — 이 Fragment/Activity 에 묶인 것
  • by activityViewModels() — 호스트 Activity 에 묶인 것 (Fragment 끼리 공유)
  • by hiltNavGraphViewModels(R.id.flow) 네비게이션 그래프 범위

공유 범위를 넓게 잡으면 화면을 떠나도 상태가 남아 다음에 들어왔을 때 옛 데이터가 보인다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — MVVM·단방향 데이터 흐름(UDF)