
ViewModel에서 Android 종속성을 가져오는 것은 피하라는 지침은, LiveData 및 그 변환기와 같은 특정 클래스들을 예외로 두고, 깨끗한 아키텍처와 테스트 가능성의 원칙에 뿌리를 두고 있습니다. 이를 구체적으로 살펴보면 다음과 같습니다.
Android 종속성, 예를 들어 R(리소스)과 같은 Android 프레임워크 클래스들은 Android 운영체제 및 그 컨텍스트와 직접적으로 연결되어 있습니다. 이러한 클래스들은 UI 요소 관리, Android 리소스(문자열, 드로어블 리소스 등) 접근, 그리고 Android 시스템(Intent, Context)과의 상호작용을 담당합니다.
ViewModel에서 Android 종속성을 배제하면 관심사의 분리가 명확해집니다. ViewModel은 Android 컨텍스트나 UI 요소에 대해 알 필요가 없으며, 오로지 UI를 위한 데이터를 관리하는데 집중할 수 있습니다. 이는 ViewModel이 UI 관련 로직과 독립적으로 동작할 수 있게 하며, ViewModel을 더욱 재사용 가능하고, 독립적인 테스트가 가능하게 만듭니다.
그러나 ViewModel에서 상태의 일부로 문자열을 방출해야 한다면?
이런 경우에는 Kotlin의 sealed interface를 사용하는 것이 이상적입니다. sealed interface를 사용하면 상태를 더욱 구조적으로 정의할 수 있으며, 상태마다 다른 유형의 데이터를 포함할 수 있습니다. 이 방법을 사용하면 ViewModel에서 Android 종속성 없이도 문자열을 상태의 일부로 안전하게 관리할 수 있습니다.
sealed interface를 활용하는 방법은 아래에 링크로 두겠습니다.
https://velog.io/@heetaeheo/Android%EC%97%90%EC%84%9C%EC%9D%98-%EC%BD%94%ED%8B%80%EB%A6%B0-Sealed-Interface
Android 종속성은 유닛 테스트를 더 복잡하게 만들 수 있습니다. 왜냐하면 이러한 종속성은 종속 Android 환경(에뮬레이터나 실제 기기)을 필요로 하기 때문입니다. 이로 인해 테스트 속도가 느려지고, 테스트가 더 취약해질 수 있습니다.
반면, Android 종속성이 없는 ViewModel은 JVM에서 Android 환경 없이 테스트할 수 있어, 더 빠르고 신뢰할 수 있는 테스트를 수행할 수 있습니다. LiveData와 그 변환기 (transformers)는 예외입니다. 왜냐하면 이들은 생명주기 인식 (lifecycle-aware)으로 설계되어 있으며, 테스트에서 쉽게 목(mock)하거나 관찰할 수 있기 때문입니다.
이러한 이유로 ViewModel에서 Android 종속성을 최소화하고, 테스트 가능한 코드 베이스를 유지하는 것이 중요합니다.
Android 프레임워크에 직접적으로 의존하지 않는 코드는 더 이식성이 높고 유지 관리가 쉬워집니다. 이 코드는 애플리케이션의 다른 부분에서 재사용할 수 있으며, 최소한의 변경으로 다른 프로젝트에서도 활용할 수 있습니다. 또한, 미래에는 ViewModel을 Slack의 Circuit Presenters로 쉽게 교체하여 코드를 Kotlin Multi-Platform으로 마이그레이션 할 수도 있습니다.
LiveData와 그 변환기 (Transformations.map, Transformations.switchMap )는 ViewModel에서 사용하도록 설계되었습니다. 이들은 생명주기 인식(lifecycle-aware)으로, ViewModel이 액티비티나 프래그먼트의 생명주기 이벤트에 안전하게 반응하면서 UI를 업데이트할 수 있도록 합니다.
이 클래스들은 ViewModel을 특정 Android UI요소나 리소스에 묶지 않습니다. 대신, ViewModel이 생명주기를 인식하면서 데이터 변경을 방출하는 메커니즘을 제공하며, UI 계층이 이를 관찰할 수 있도록 합니다. 이로 인해 ViewModel이 Android 종속성 없이도 UI와의 통신을 효율적으로 처리할 수 있게 됩니다.
ViewModel 생성자에서 의존성을 지연 초기화 없이 직접 주입하면 다음과 같은 문제가 발생할 수 있습니다.
지연 초기화(Lazy Initialization)의 이점
지연 초기화는 의존성이 실제로 필요해질 때까지 초기화를 미루어, 앱의 성능과 효율성을 최적화합니다. 이 접근 방식은 크고, 잘 사용되지 않거나 조건적으로 필요한 의존성에 유리합니다. 의존성 사용 사례에 따라 즉각적인 초기화와 지연 초기화를 균형있게 사용하는 것이 최적의 앱 성능을 위해 중요합니다.
지연 초기화를 사용하지 않는 것이 미치는 영향
ViewModel 생성자에서 의존성에 대해 지연 초기화를 사용하지 않으면, 특히 앱 시작 시 또는 ViewModel 인스턴스를 생성할 때 Android 애플리케이션의 성능과 자원 활용에 부정적인 영향을 미칠 수 있습니다.
지연 초기화를 사용하는 경우
이러한 이유로, 의존성의 초기화 시점을 신중하게 조정하여 앱의 성능과 리소스 활용도를 최적화하는 것이 중요합니다.
예시: 북마크 기능의 지연 초기화
@HiltViewModel
class BookViewModel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val bookmarkUseCase: dagger.Lazy<BookmarkUsecase>,
북마크 버튼을 클릭했을 때 ViewModel에서 북마크 동작이 트리거 되는 상황을 상상해보세요. 이 경우, BookViewModel 생성자를 통해 주입되는 의존성 (예: 북마크를 처리하는 UseCase)에 대해 지연 초기화를 활용할 수 있습니다. 이를 통해 사용자가 실제로 북마크 버튼을 클릭할 때까지 Usecase의 생성이 지연됩니다.
class BookViewModel(
private val bookmarkUseCase: Lazy<BookmarkUseCase>
) : ViewModel() {
fun onBookmarkClicked() {
// bookmarkUseCase는 실제로 호출될 때 초기화됩니다.
bookmarkUseCase.value.executeBookmarkAction()
}
}