[Android] HiltViewModel과 ViewModel의 차이
ViewModel과 HiltViewModel의 차이점은 무엇일까?
LiveData와 StateFlow 둘 중 어느 것을 사용해야 될까?
Sep 02, 2024
![[Android] HiltViewModel과 ViewModel의 차이](https://image.inblog.dev?url=https%3A%2F%2Finblog.ai%2Fapi%2Fog%3Ftitle%3D%255BAndroid%255D%2520HiltViewModel%25EA%25B3%25BC%2520ViewModel%25EC%259D%2598%2520%25EC%25B0%25A8%25EC%259D%25B4%26logoUrl%3Dhttps%253A%252F%252Finblog.ai%252Finblog_logo.png%26blogTitle%3Dcode-with-me&w=2048&q=75)
1. ViewModel과 HiltViewModel의 차이점기본 ViewModelHiltViewModel🔷 주요 차이점2. LiveData와 StateFlow 비교LiveDataStateFlow✅ 어떤 것을 선택해야 할까?🔷 LiveData가 적합한 경우🔷 StateFlow (또는 SharedFlow)가 적합한 경우✅ 결론 요약3. 예시 코드 : 양쪽 모두 사용하기
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