[Android] HiltViewModel과 ViewModel의 차이
ViewModel과 HiltViewModel의 차이점은 무엇일까?
LiveData와 StateFlow 둘 중 어느 것을 사용해야 될까?
정확하게 동작하는 지 확인하기 위한 테스트에 대해서 알아보자.
1. ViewModel 개요1-1. ViewModel의 이점2. HiltViewModel의 개요2-1. HiltViewModel의 이점3. HiltViewModel과 일반 ViewModel의 차이점4. LiveData vs. StateFlow5. 테스트HiltViewModel 테스트 방법
1. ViewModel 개요
ViewModel
클래스는 비즈니스 로직 또는 화면 수준 상태 홀더(business logic or screen level state holder)이다. UI에 상태를 노출하고 관련 비즈니스 로직을 캡슐화한다.
- Jetpack Compose를 사용할 때 ViewModel은 화면 UI 상태를 컴포저블에 노출하는 기본 수단이다. Compose와 함께 ViewModel을 사용할 때 유의해야 할 가장 중요한 점은 ViewModel의 범위를 컴포저블로 지정할 수 없다는 것이다. Compose에서 ViewModel의 이점을 얻으려면 Fragment나 Activity에서 각 화면을 호스팅하거나, Compose Navigation을 사용하고 탐색 대상에 최대한 가깝게 구성 가능한 함수에서 ViewModel을 사용해야 한다. ViewModel의 범위를 탐색 대상, 탐색 그래프, 활동, 프래그먼트로 지정할 수 있기 때문이다.
import androidx.activity.viewModels class DiceRollActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { // Create a ViewModel the first time the system calls an activity's onCreate() method. // Re-created activities receive the same DiceRollViewModel instance created by the first activity. // Use the 'by viewModels()' Kotlin property delegate // from the activity-ktx artifact val viewModel: DiceRollViewModel by viewModels() lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { // Update UI elements } } } } } data class DiceUiState( val firstDieValue: Int? = null, val secondDieValue: Int? = null, val numberOfRolls: Int = 0, ) class DiceRollViewModel : ViewModel() { // Expose screen UI state private val _uiState = MutableStateFlow(DiceUiState()) val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow() // Handle business logic fun rollDice() { _uiState.update { currentState -> currentState.copy( firstDieValue = Random.nextInt(from = 1, until = 7), secondDieValue = Random.nextInt(from = 1, until = 7), numberOfRolls = currentState.numberOfRolls + 1, ) } } }
1-1. ViewModel의 이점
ViewModel의 대안은 UI에 표시되는 데이터를 보유하는 일반 클래스입니다. 이는 활동이나 탐색 대상 간에 이동할 때 문제가 될 수 있습니다. 이렇게 하면 인스턴스 상태 저장 메커니즘을 사용하여 데이터를 저장하지 않을 경우 해당 데이터가 소멸됩니다.
ViewModel은 데이터 지속성을 위한 편리한 API를 제공하여 이 문제를 해결합니다. ViewModel 클래스의 주요 이점은 기본적으로 두 가지입니다.
- UI 상태를 유지할 수 있습니다.
- 비즈니스 로직에 대한 액세스 권한을 제공합니다.
- 첫 번째 문단을 보면 ViewModel의 장점을 강조하기 위해, ViewModel이 없을 때 발생할 수 있는 문제점을 제시하고 이를 해결하기 위해 ViewModel이 필요하다는 점을 명확히 하려는 것 같다.
- ViewModel이 없을 때 발생할 수 있는 문제 상황을 예시로 제시한다.
- UI에 표시되는 데이터를 보유하는 일반 클래스는 활동이나 탐색 대상 간에 이동할 때 문제가 될 수 있음
- 인스턴스 상태 저장 메커니즘을 사용하여 데이터를 저장하지 않을 경우 해당 데이터가 소멸함
더 쉬운 설명 (Gemini.ver)
- 문제 상황 : 예전에는 앱 화면을 이동할 때마다 데이터를 새로 만들거나, 화면이 바뀔 때마다 데이터가 사라지는 문제가 있었어요. 마치 게임에서 캐릭터가 다른 방으로 이동할 때마다 다시 처음부터 시작하는 것처럼 말이죠.
- ViewModel의 해결책 : ViewModel은 이런 문제를 해결하기 위해 등장했어요. 마치 게임에서 캐릭터가 어떤 방에 있든, 가지고 있는 아이템이나 레벨은 유지되는 것처럼, ViewModel은 앱 화면이 바뀌더라도 데이터를 안전하게 보관해주는 역할을 합니다.
- 비유를 통한 설명 : ViewModel은 마치 데이터의 '보관함'과 같아요. 앱 화면이 바뀌어도 이 보관함은 그대로 유지되기 때문에, 언제든지 필요한 데이터를 꺼내 쓸 수 있습니다.
- 구체적인 예시 : 예를 들어, 쇼핑 앱에서 상품 목록을 보다가 상세 페이지로 이동했다가 다시 목록 페이지로 돌아왔을 때, 이전에 보던 상품 목록이 그대로 유지되는 것이 바로 ViewModel 덕분입니다.
2. HiltViewModel의 개요
HiltViewModel은 Android에서 의존성 주입을 위한 라이브러리인 Hilt를 사용하여 ViewModel을 생성하고 관리하는 방법이다.
ViewModel은 MVVM 패턴에서 UI와 데이터를 분리하여 관리하는 역할을 하며, HiltViewModel은 이러한 ViewModel을 더욱 효율적으로 관리할 수 있도록 도와준다.
2-1. HiltViewModel의 이점
@HiltViewModel class MyViewModel @Inject constructor( private val repository: MyRepository ) : ViewModel() { // @HiltViewModel 어노테이션은 이 클래스가 Hilt에 의해 관리되는 ViewModel임을 나타냄 // @Inject 어노테이션은 repository 의존성이 Hilt에 의해 자동으로 주입될 것을 의미 }
- 의존성 주입 간소화:
- Hilt를 사용하면 ViewModel에 필요한 의존성을 간단하게 주입할 수 있습니다.
- 복잡한 의존성 그래프를 직접 관리할 필요 없이, Hilt가 알아서 의존성을 해결해 줍니다.
- 코드 가독성 향상:
- 의존성 주입 코드가 줄어들어 코드가 더욱 깔끔하고 가독성이 좋아집니다.
- ViewModel에 집중하여 비즈니스 로직을 구현할 수 있습니다.
- 테스트 용이성 증대:
- 의존성을 명확하게 주입하기 때문에 ViewModel을 쉽게 테스트할 수 있습니다.
- 모킹 라이브러리를 사용하여 의존성을 가짜 객체로 바꾸고, ViewModel의 동작을 정확하게 검증할 수 있습니다.
- 유지보수성 향상:
- 의존성 관리가 체계화되어 코드 변경 시 발생할 수 있는 문제를 줄일 수 있습니다.
- 모듈화 지원:
- Hilt는 Android 애플리케이션을 모듈화하는 것을 지원합니다.
- 각 모듈마다 독립적인 의존성 그래프를 구성하여 코드를 더욱 잘 관리할 수 있습니다.
3. HiltViewModel과 일반 ViewModel의 차이점
HiltViewModel은 Android의 의존성 주입 라이브러리인 Hilt를 사용하여 생성하고 관리되는 ViewModel이다. 일반 ViewModel과 비교하여 HiltViewModel은 더욱 효율적이고 안정적인 의존성 관리를 제공하며, 코드의 가독성과 유지보수성을 향상시킨다.
특징 | 일반 ViewModel | HiltViewModel |
의존성 주입 | 수동으로 의존성을 생성하고 주입 | @Inject 어노테이션을 사용하여 의존성을 선언하면 Hilt가 자동으로 의존성을 생성하고 주입 |
코드 가독성 | 의존성 생성 및 주입 로직이 ViewModel 코드에 포함되어 가독성이 떨어질 수 있음 | 의존성 관련 코드가 줄어들어 ViewModel 코드에 오직 비즈니스 로직에만 집중할 수 있음 |
테스트 용이성 | 의존성을 모킹하기 위해 Mockito와 같은 모킹 라이브러리를 사용해야 하고, 추가적인 설정이 필요함 | Hilt의 테스트 모듈을 사용하여 의존성을 쉽게 모킹하고 테스트 케이스를 작성할 수 있음 |
유지보수성 | 의존성이 변경될 때마다 ViewModel의 생성자를 수정해야 함 | Hilt가 의존성 그래프를 관리하기 때문에 의존성 변경에 따른 영향을 최소화할 수 있음 |
모듈화 지원 | 모듈화를 위한 추가적인 설정이 필요하고, 의존성 관리가 복잡해질 수 있음 | Hilt의 모듈 시스템을 활용하여 자연스럽게 모듈화 가능 |
4. LiveData vs. StateFlow
ViewModel에서 LiveData와 StateFlow 중 어떤 것을 선택해야 할까? 둘 다 데이터 흐름을 관리하는 데 유용한 도구이지만 각각의 특징과 장단점이 다르다.
- LiveData를 선택해야 하는 경우
- Android 앱에서 UI 상태를 관리하는 것이 주된 목표일 때 : LiveData는 Android 생명주기를 잘 인식하고, UI 구성 요소를 자동으로 업데이트하는 데 특화되어 있다.
- 간단한 데이터 흐름을 구현하고 싶을 때 : LiveData는 사용법이 간단하고, Android 개발자에게 친숙하기 때문에 빠르게 개발을 진행할 수 있다.
- 기존 프로젝트에서 LiveData를 사용하고 있고, 큰 변경 없이 유지보수하고 싶을 때 : LiveData를 계속 사용하는 것이 더 효율적일 수 있다.
- StateFlow를 선택해야 하는 경우
- Kotlin Coroutines를 활용하여 더욱 복잡한 데이터 흐름을 관리하고 싶을 때 : StateFlow는 Kotlin Coroutines와의 연동이 뛰어나며, 다양한 연산자를 활용하여 복잡한 데이터 흐름을 구현할 수 있습니다.
- 플랫폼에 독립적인 코드를 작성하고 싶을 때 : StateFlow는 Kotlin의 표준 라이브러리이므로, Android뿐만 아니라 다른 플랫폼에서도 사용할 수 있다.
- 백프레셔를 활용하여 데이터 흐름을 제어해야 할 때 : StateFlow는 백프레셔를 지원하여 과도한 데이터 생성으로 인한 메모리 누수를 방지할 수 있다.
결론
- 간단한 UI 상태 관리: LiveData
- 복잡한 데이터 흐름 관리, 플랫폼 독립성: StateFlow
가이드라인
- 팀 내 규약: 팀 내에서 LiveData 또는 StateFlow 중 어떤 것을 주로 사용할지 미리 정해놓는 것이 좋다.
- 프로젝트의 복잡도: 프로젝트의 규모와 복잡도에 따라 적절한 도구를 선택해야 한다.
- 학습 비용: StateFlow는 LiveData보다 학습 곡선이 조금 더 가파를 수 있다.
- 새로운 프로젝트를 시작할 때: StateFlow를 사용하여 미래를 위한 유연성 확보
- 기존 프로젝트에 LiveData를 사용하고 있을 때: 당장 StateFlow로 모두 변경하기보다는 필요한 부분부터 점진적으로 변경
- 팀의 기술 스택: 팀원들의 Kotlin Coroutines에 대한 이해도를 고려하여 선택
5. 테스트
왜 ViewModel을 테스트해야 할까?
ViewModel은 Android 앱에서 UI와 데이터를 분리하여 관리하는 중요한 역할을 한다. 따라서 ViewModel이 정확하게 동작하는지 확인하기 위한 테스트는 필수적이다
- 버그 조기 발견
- 코드 품질 향상
- 리팩토링 안전성 확보
HiltViewModel 테스트 방법
@HiltAndroidTest class MyViewModelTest { @get:Rule var coroutinesRule = CoroutinesTestRule() @Inject lateinit var viewModel: MyViewModel @Before fun setUp() { // Hilt 테스트 컨테이너 초기화 HiltAndroidRule(this).inject() } @Test fun `increment count`() = runBlockingTest { // Given val expectedCount = 1 // When viewModel.increment() // Then assertEquals(expectedCount, viewModel.count.value) } }
1. 테스트 환경 설정
- Hilt 테스트 모듈 : Hilt를 사용하여 테스트 환경을 설정합니다. 테스트 모듈에서 테스트에 필요한 의존성을 제공하고, 실제 구현체 대신 모킹 객체를 제공합니다.
- Junit, Mockito 등 : 테스트 프레임워크와 모킹 라이브러리를 사용하여 테스트 케이스를 작성
2. 의존성 모킹
- Hilt의
@MockK
annotation: Hilt는@MockK
어노테이션을 제공하여 간편하게 모킹 객체를 생성할 수 있습니다.
- Mockito와 같은 모킹 라이브러리를 사용하여 더욱 복잡한 모킹 시나리오 구현
3. 테스트 케이스 작성
- Given-When-Then : 테스트 케이스를 작성할 때 Given-When-Then 패턴을 사용하여 가독성을 높이고 테스트의 의도를 명확하게 전달합니다.
- LiveData 관찰 : LiveData를 사용하는 경우 LiveData의 값이 변경되는 것을 관찰하고 예상한 결과와 비교합니다.
- Coroutine 테스트 : Coroutine을 사용하는 경우 Coroutine 테스트를 위한 라이브러리(예: kotlinx-coroutines-test)를 사용하여 테스트를 수행합니다.
Share article