Android ViewModel 안티패턴 (3)

HEETAE HEO·2024년 10월 29일
post-thumbnail

ViewModel에서 Android 종속성은 가능한 한 피하라

ViewModel에서 Android 종속성을 가져오는 것은 피하라는 지침은, LiveData 및 그 변환기와 같은 특정 클래스들을 예외로 두고, 깨끗한 아키텍처와 테스트 가능성의 원칙에 뿌리를 두고 있습니다. 이를 구체적으로 살펴보면 다음과 같습니다.

1. 관심사의 분리 (Separation of Concerns)

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

2. 테스트 가능성 (Testability)

Android 종속성은 유닛 테스트를 더 복잡하게 만들 수 있습니다. 왜냐하면 이러한 종속성은 종속 Android 환경(에뮬레이터나 실제 기기)을 필요로 하기 때문입니다. 이로 인해 테스트 속도가 느려지고, 테스트가 더 취약해질 수 있습니다.

반면, Android 종속성이 없는 ViewModel은 JVM에서 Android 환경 없이 테스트할 수 있어, 더 빠르고 신뢰할 수 있는 테스트를 수행할 수 있습니다. LiveData와 그 변환기 (transformers)는 예외입니다. 왜냐하면 이들은 생명주기 인식 (lifecycle-aware)으로 설계되어 있으며, 테스트에서 쉽게 목(mock)하거나 관찰할 수 있기 때문입니다.

이러한 이유로 ViewModel에서 Android 종속성을 최소화하고, 테스트 가능한 코드 베이스를 유지하는 것이 중요합니다.

3. 이식성 (Portability)

Android 프레임워크에 직접적으로 의존하지 않는 코드는 더 이식성이 높고 유지 관리가 쉬워집니다. 이 코드는 애플리케이션의 다른 부분에서 재사용할 수 있으며, 최소한의 변경으로 다른 프로젝트에서도 활용할 수 있습니다. 또한, 미래에는 ViewModel을 Slack의 Circuit Presenters로 쉽게 교체하여 코드를 Kotlin Multi-Platform으로 마이그레이션 할 수도 있습니다.

그렇다면 LiveData와 변환기는 어떨까요?

LiveData와 그 변환기 (Transformations.map, Transformations.switchMap )는 ViewModel에서 사용하도록 설계되었습니다. 이들은 생명주기 인식(lifecycle-aware)으로, ViewModel이 액티비티나 프래그먼트의 생명주기 이벤트에 안전하게 반응하면서 UI를 업데이트할 수 있도록 합니다.

이 클래스들은 ViewModel을 특정 Android UI요소나 리소스에 묶지 않습니다. 대신, ViewModel이 생명주기를 인식하면서 데이터 변경을 방출하는 메커니즘을 제공하며, UI 계층이 이를 관찰할 수 있도록 합니다. 이로 인해 ViewModel이 Android 종속성 없이도 UI와의 통신을 효율적으로 처리할 수 있게 됩니다.

5. 생성자에서 의존성을 지연초기화 하지 않는 것:

ViewModel 생성자에서 의존성을 지연 초기화 없이 직접 주입하면 다음과 같은 문제가 발생할 수 있습니다.

  • 시작 시간 증가: 앱이 시작될 때 모든 의존성이 즉시 초기화되므로, 전체 시작 시간이 길어질 수 있습니다.
  • 높은 메모리 사용량: 필요하지 않은 의존성도 초기화되기 때문에, 불필요하게 메모리를 소비하게 됩니다.
  • 불필요한 CPU 자원 사용: 사용되지 않는 의존성을 초기화하기 위해 CPU 리소스가 낭비될 수 있습니다.

지연 초기화(Lazy Initialization)의 이점

지연 초기화는 의존성이 실제로 필요해질 때까지 초기화를 미루어, 앱의 성능과 효율성을 최적화합니다. 이 접근 방식은 크고, 잘 사용되지 않거나 조건적으로 필요한 의존성에 유리합니다. 의존성 사용 사례에 따라 즉각적인 초기화와 지연 초기화를 균형있게 사용하는 것이 최적의 앱 성능을 위해 중요합니다.

지연 초기화를 사용하지 않는 것이 미치는 영향

ViewModel 생성자에서 의존성에 대해 지연 초기화를 사용하지 않으면, 특히 앱 시작 시 또는 ViewModel 인스턴스를 생성할 때 Android 애플리케이션의 성능과 자원 활용에 부정적인 영향을 미칠 수 있습니다.

지연 초기화를 사용하는 경우

  • 큰 의존성 또는 잘 사용되지 않는 의존성: ViewModel이 큰 의존성을 가지고 있거나, 자주 접근되지 않는 의존성을 가지고 있을 경우, 지연 초기화가 유리합니다. 이는 이러한 리소스가 실제로 필요할 때까지 초기화 비용을 피할 수 있게 해줍니다.
  • 조건부 의존성: 특정 조건에서만 필요한 의존성(예: 사용자 행동이나 특정 앱 상태에 따라 필요한 경우)에 대해서는, 지연 초기화를 통해 불필요한 설정을 방지할 수 있습니다.

이러한 이유로, 의존성의 초기화 시점을 신중하게 조정하여 앱의 성능과 리소스 활용도를 최적화하는 것이 중요합니다.

예시: 북마크 기능의 지연 초기화

@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()
    }
}
profile
Android 개발 잘하고 싶어요!!!

0개의 댓글