[Android] HiltViewModel과 ViewModel의 차이

ViewModel과 HiltViewModel의 차이점은 무엇일까? LiveData와 StateFlow 둘 중 어느 것을 사용해야 될까?
vmkmym's avatar
Sep 02, 2024
[Android] HiltViewModel과 ViewModel의 차이

1. ViewModel과 HiltViewModel의 차이점

기본 ViewModel

ViewModel은 Android Jetpack 아키텍처 컴포넌트의 일부로, UI 관련 데이터를 저장하고 관리하기 위해 설계되었습니다.
 
ViewModel의 주요 특징 :
  • 화면 회전과 같은 구성 변경에도 데이터가 유지됩니다.
  • ViewModel은 생명주기 인식을 직접 하진 않지만, 관련된 Activity나 Fragment가 완전히 종료될 때 함께 소멸되며, 이 과정에서 viewModelScope를 통해 내부 작업이 자동으로 정리됩니다.
  • UI 컨트롤러와 데이터 처리 로직을 분리하여 테스트 용이성과 코드 가독성을 높입니다.
 
기본 ViewModel을 사용하면 다음과 같이 구현합니다.
class MainViewModel : ViewModel() { private val _count = MutableLiveData<Int>(0) val count: LiveData<Int> = _count fun increment() { _count.value = (_count.value ?: 0) + 1 } override fun onCleared() { super.onCleared() // 리소스 정리 코드 } }
 
액티비티나 프래그먼트에서는 다음과 같이 사용합니다.
class MainActivity : AppCompatActivity() { private lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) // 관찰 설정 viewModel.count.observe(this) { count -> counterTextView.text = count.toString() } } }
 

HiltViewModel

@HiltViewModel은 Hilt 의존성 주입 라이브러리에서 제공하는 어노테이션으로, ViewModel에 의존성 주입을 쉽게 적용할 수 있게 해줍니다. Hilt는 Dagger를 Android에 맞게 최적화한 의존성 주입 라이브러리입니다.
 
HiltViewModel의 주요 이점 :
  • 의존성 주입이 간소화됩니다.
  • 보일러플레이트 코드를 줄일 수 있습니다.
  • ViewModel에 복잡한 의존성이 있을 때 특히 유용합니다.
  • ViewModelFactory를 직접 구현할 필요가 없습니다.
 
HiltViewModel을 사용한 예제
@HiltViewModel class MainViewModel @Inject constructor( private val repository: UserRepository, private val analyticsTracker: AnalyticsTracker ) : ViewModel() { private val _userData = MutableLiveData<User>() val userData: LiveData<User> = _userData init { loadUserData() } private fun loadUserData() { viewModelScope.launch { try { _userData.value = repository.getUser() analyticsTracker.trackEvent("user_data_loaded") } catch (e: Exception) { // 오류 처리 } } } }
 
액티비티나 프래그먼트에서는 다음과 같이 간단하게 사용할 수 있습니다.
@AndroidEntryPoint class MainActivity : AppCompatActivity() { // 자동으로 의존성이 주입됩니다 private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 관찰 설정 viewModel.userData.observe(this) { user -> userNameTextView.text = user.name } } }
 

🔷 주요 차이점

특성
ViewModel
HiltViewModel
의존성 주입
수동 관리 필요
Hilt가 자동으로 관리
Factory 필요 여부
복잡한 의존성이 있는 경우 Factory 필요
Factory 필요 없음
코드량
의존성이 많을수록 보일러플레이트 증가
간결한 코드 유지
테스트 용이성
Mock 객체 주입이 복잡할 수 있음
테스트에 필요한 가짜 의존성 주입이 용이
학습 곡선
비교적 낮음
Dagger/Hilt 개념 이해 필요

 

2. LiveData와 StateFlow 비교

Android에서 UI 상태를 관리하는 데 사용되는 두 가지 주요 옵션인 LiveData와 StateFlow의 차이점과 각각의 사용 사례를 알아보겠습니다.
 

LiveData

LiveData는 Android Architecture Components의 일부로, 생명주기를 인식하는 관찰 가능한 데이터 홀더 클래스입니다.
 
장점
  • 생명주기 인식: UI 컴포넌트가 활성 상태일 때만 업데이트를 전달합니다.
  • 메모리 누수 방지: 자동으로 Observer를 정리합니다.
  • 구성 변경 시 자동 데이터 복구: 화면 회전 등에서 유용합니다.
  • Android 특화: 안드로이드 플랫폼에 최적화되어 있습니다.
 
단점
  • 안드로이드 플랫폼에 종속적입니다.
  • Kotlin 코루틴과의 통합이 기본적으로 제공되지 않습니다.
  • 초기값 설정이 필수가 아니며, null을 허용합니다.
  • 연산자 체인이 제한적입니다.
class ProfileViewModel : ViewModel() { private val _userProfile = MutableLiveData<UserProfile>() val userProfile: LiveData<UserProfile> = _userProfile fun loadProfile(userId: String) { viewModelScope.launch { val result = repository.getUserProfile(userId) _userProfile.value = result } } } // 사용 예시 viewModel.userProfile.observe(viewLifecycleOwner) { profile -> nameTextView.text = profile.name emailTextView.text = profile.email }
 

StateFlow

StateFlow는 Kotlin 코루틴의 Flow API의 일부로, 상태를 나타내는 값의 흐름을 제공합니다.
 
장점
  • 코루틴과 완벽하게 통합됩니다.
  • 다양한 연산자(map, filter, combine 등)를 지원합니다.
  • 초기값이 필수적이라 null 안전성이 높습니다.
  • 안드로이드 외부 로직에서도 사용할 수 있습니다.
  • 더 많은 제어 기능을 제공합니다(distinctUntilChanged() 기본 적용).
 
단점
  • 생명주기 인식이 기본 제공되지 않아 별도 설정이 필요합니다.
  • viewLifecycleOwner.lifecycleScope.launch { ... } 같은 콜렉터 설정이 필요합니다.
  • 초기값 설정이 필수적입니다.
class ProfileViewModel : ViewModel() { private val _userProfile = MutableStateFlow<UserProfile>(UserProfile.Empty) val userProfile: StateFlow<UserProfile> = _userProfile.asStateFlow() fun loadProfile(userId: String) { viewModelScope.launch { val result = repository.getUserProfile(userId) _userProfile.value = result } } } // 사용 예시 lifecycleScope.launch { // 생명주기 인식 별도 설정 lifecycleScope, repeatOnLifecycle repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.userProfile.collect { profile -> nameTextView.text = profile.name emailTextView.text = profile.email } } } // 또는 viewLifecycleOwner와 launchWhenStarted 사용 viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.userProfile.collect { profile -> nameTextView.text = profile.name emailTextView.text = profile.email } } }
 

✅ 어떤 것을 선택해야 할까?

🔷 LiveData가 적합한 경우

  • Android UI와만 상호작용하는 단순한 앱을 만들 때
  • 생명주기 인식이 중요한 경우 (UI에 필요한 데이터만 안전하게 전달하고 싶을 때)
  • ViewModel, Navigation, DataBinding 등 Android 아키텍처 컴포넌트와의 호환성이 중요한 경우
  • 코루틴을 사용하지 않고 간단한 MVVM 패턴을 빠르게 구성하고자 할 때
  • Android 플랫폼에 종속적인 구조가 문제가 되지 않는 경우
💡 LiveData는 생명주기를 자동으로 인식하며, observe()를 통해 안전하고 간편하게 UI에 데이터를 전달할 수 있습니다.
 

🔷 StateFlow (또는 SharedFlow)가 적합한 경우

  • 프로젝트 전반에 코루틴을 적극적으로 사용하는 경우
  • map, combine, flatMapLatest 등 **복잡한 데이터 흐름 처리(스트림 변환)**가 필요한 경우
  • 안드로이드 외부에서도 재사용 가능한 코드를 작성하고 싶을 때 (예: Kotlin Multiplatform 등)
  • 반응형 프로그래밍 패턴을 더 깊게 활용하고 싶을 때
  • *세밀한 제어(딜레이, 예외처리, 백프레셔 등)**가 필요한 경우
  • *이벤트 처리(SharedFlow)**나 **상태 관리(StateFlow)**가 분명히 나뉘어야 하는 경우
💡 Flow는 생명주기를 자동으로 인식하지 않기 때문에 repeatOnLifecycle 등을 사용해 명시적으로 생명주기 관리가 필요합니다.
하지만 그만큼 유연하고 플랫폼 독립적이며, 재사용성이 높은 코드 구성이 가능합니다.
 

✅ 결론 요약

상황
LiveData
StateFlow / SharedFlow
생명주기 자동 인식
✅ 지원
❌ 직접 처리 필요
안드로이드 전용 여부
✅ 전용 (Android 의존)
❌ 독립적 (Kotlin 표준)
코루틴 사용 여부
❌ 필요 없음
✅ 필수
간단한 MVVM 앱
✅ 적합
❌ 과할 수 있음
스트림 변환/복잡한 로직
❌ 제한적
✅ 강력함
KMP, 서버, 백엔드 등과의 호환성
❌ 어려움
✅ 유리함
 
Android 앱 개발 추세
  • 최신 Android 앱 개발에서는 StateFlow와 코루틴을 함께 사용하는 경향이 증가하고 있습니다.
  • Jetpack Compose와 같은 최신 UI 툴킷은 StateFlow와 더 잘 통합됩니다.
  • Google도 점차 Flow 기반 접근 방식을 권장하고 있습니다.
 
 

3. 예시 코드 : 양쪽 모두 사용하기

@HiltViewModel class WeatherViewModel @Inject constructor( private val weatherRepository: WeatherRepository, private val locationTracker: LocationTracker ) : ViewModel() { // StateFlow를 사용한 현재 날씨 상태 private val _weatherState = MutableStateFlow<WeatherState>(WeatherState.Loading) val weatherState: StateFlow<WeatherState> = _weatherState.asStateFlow() // LiveData를 사용한 위치 정보 (시스템 서비스에서 오는 데이터) private val _locationUpdates = MutableLiveData<Location>() val locationUpdates: LiveData<Location> = _locationUpdates init { // 위치 업데이트를 관찰 viewModelScope.launch { locationTracker.getLocationUpdates().collect { location -> _locationUpdates.value = location fetchWeatherForLocation(location) } } } private fun fetchWeatherForLocation(location: Location) { viewModelScope.launch { _weatherState.value = WeatherState.Loading try { val weather = weatherRepository.getWeatherForLocation( location.latitude, location.longitude ) _weatherState.value = WeatherState.Success(weather) } catch (e: Exception) { _weatherState.value = WeatherState.Error(e.message ?: "Unknown error") } } } // sealed class로 날씨 상태를 모델링 sealed class WeatherState { object Loading : WeatherState() data class Success(val weather: Weather) : WeatherState() data class Error(val message: String) : WeatherState() } }
 
Share article

code-with-me