안드로이드 MVVM 따라 하기 - MVVM에서의 안티 패턴



개인 광고 영역

Android MVVM 따라 하기 3 번째 글이다.

이번 글에서는 최소한 하지 않아야 할 부분을 적어보려고 한다.

안드로이드 MVVM은 결국 Lifecycle을 알고는 있다. 하지만 Context를 직접 접근하지는 않아야 한다.

class SampleViewModel {

  fun showMain(context: Context) {
    // Bad
  }

단순 데이터 전달이 없다면 어떠한 이벤트를 바로 View에서 처리하는 편이 더 좋다.

하지만 데이터가 추가로 필요한 경우, 상황에 따라 다른 화면으로 이동을 필요로 한다면 다음과 같이 작성하는 게 좋다.

  private val _showMain = MutableLiveData<Item?>(null)
  val showMain: LiveData<Item?>() get() = _showMain
  // or
  private val _showMain = MutableStateFlow<Item?>(null)
  val showMain: Flow<Item> get() = _showMain.filterNotNull()

  fun loadData() {
    // Load data
    _showMain.value = Item(...)
  }
}


이 글에서 알아볼 내용

  • ViewModel에서 할 수 있는 것과 하지 말아야 할 것은?
  • Context를 넘겨주면 안 되는 이유는?


최소한 다음은 하지 말자

MVP와 MVVM에서 아주 잘못 활용하고 있다고 생각되는 부분은 바로 Context, Application을 주입받는 일이다.

Context나 Application을 주입받으면 어떠한 일을 할 수 있을까?

당연하지만 Context와 Application을 이용한 모든 일을 할 수 있다.

  • Context를 Activity로 형 변환 할 수 있다.
  • Context를 이용한 권한 획득 요청할 수 있다.
  • Context를 이용한 Resource에 접근할 수 있다.
  • Context를 이용한 다른 화면으로의 전환도 가능하다.

작성한 내용을 보면 원래 Activity나 Fragment에서 하던 일들을 ViewModel에서도 그대로 할 수 있다.

최소한 이는 하지 않아야 한다. 특히나 AAC-ViewModel을 활용하는 경우라면 더더욱?

하지만 ViewModel도 Activity Lifecycle 상태를 알 수 있는데, create 시점인지 destroy 시점인지는 알 수 있다.

이를 활용해 메모리를 초기화하거나, 릴리즈 하라는 것일 뿐 이를 통해 context에 접근하는 것과는 다르다.

MVP/MVVM의 목적은 Activity/Fragment의 코드를 줄이는 데 있지 않다.

자연스럽게 줄어들긴 하겠지만 뷰와 뷰를 표현하기 위한 로직의 분리가 핵심 목적이다.

테스트 관련 글은 이전에 작성한 글을 참고.

안드로이드 MVVM에서의 테스트 검증을 더 잘하려면?


AAC-ViewModel을 써야 할까? ViewModel을 써야 할까?

AAC-ViewModel을 활용하든 퓨어 한 ViewModel을 활용하던 목적은 동일하다.

하지만 AAC-ViewModel은 상황이 하나 다르다.

  • 화면 회전 발생 시 destroy 하지 않고, 데이터를 유지해 준다.

퓨어 한 ViewModel은 화면 회전 발생 시 직접 데이터 유지하도록 개발하지 않는 이상 유지해 주진 않는다.

참고로

AAC-ViewModel의 문서도 최근 변경된 것 같은데 원래는 화면 회전 시 데이터 유지를 위한 목적이었을 뿐이다. 하지만 수많은 개발자들과 구글의 가이드가 퓨어 한 ViewModel처럼 활용하다 보니 문서도 바뀐듯하다.

  • business logic 또는 screen level state holder로 활용한다.
  • 화면 회전 발생 시 state holder를 제공한다.

그래서 둘 중 뭘 써야 하느냐면 둘 다 상관 없다. 우리가 만드는 대부분의 앱은 화면 회전을 하지 않는다. 그러니 AAC-ViewModel을 쓰던 퓨어 한 ViewModel을 쓰던 그다지 차이가 없다는 점이다.


ViewModel 활용 시 interface로 설계하고, implementation class를 구현하는 경우

ViewModel 활용 시 interface를 정의하고, implementation class를 구현해 Context를 주입받는 일도 있을 수 있다.

android-alatan/LifecycleComponents가 이런 식으로 전달하고 있다.

ViewModel은 interface 만을 알고, 실제 구현체가 뭔지는 알지 못하는 형태로 접근한다.

이때 주의할 점이 하나 있는데


AAC-ViewModel을 쓰는 경우 주의할 점은?

화면 전환을 하거나, configuration change 상황이 발생하는 경우 AAC-ViewModel 활용에 주의할 부분이 있다.

바로 Context를 매핑해 객체를 생성하여, ViewModel에 주입하는 경우다.

Context는 Activity가 가지고 있다. configuration change가 발생하면 Context가 새로 발행된다.

그 말은 AAC-ViewModel은 유지된 상태인데, Context는 바뀌었다는 점이다.

간혹 ResourceProvider를 AAC-ViewModel과 함께 활용하는 경우를 볼 수 있는데, Activity의 Context를 활용했다면 문제가 발생할 수 있다는 소리다.

그래서 Application을 가져다 쓰는 경우도 있는데, Theme 정보가 Activity에서 만 다르다면 Resource를 불러오는 부분에서 문제가 생길 수 있겠다.

  • 설정한 UI가 안 나오는 경우

그래서 ResourceProvider를 활용하고 싶다면 이를 알고 설계해야 한다는 것이다.

하지만 실제로 화면 회전 자체가 막혀있고, configuration change 일어날 일이 많지는 않아 오류 발생 가능성도 예측하기 쉽지는 않다.


DataBinding을 써야 할까?

DataBinding은 쓰면 편하긴 하다. 하지만 Compose를 활용하기 시작하면 DataBinding은 필요치 않다.

Activity에서 이벤트를 받고, 이를 Composable에 그려주는 형태로 처리할 수 있다.

class SampleViewModel {

  val data = mutableStateOf("data")

  // fun...
}

class SampleActivity : BaseActivity() {

  @Inject
  lateinit var viewModel: SampleViewModel

  @Composable
  fun ContentView() {
    val data by remember { viewModel.data }
    // use data
  }
}

xml만 들어내면 Compose에서 좀 더 쉽게 활용하는 게 가능하다.


Di?

Di는 활용할수록 편하다. Dagger가 어렵다면 hilt를 활용할 수 있다.

필자의 경우 AAC-ViewModel 활용이 필요치 않고, 이미 내부적으로 KSP를 활용해 Dagger의 Component를 자동으로 생성하고, App에서 이들을 자동으로 연결하고 있기 때문에 hilt 보다 더 쉽게 활용하고 있다.

거기에 이전에 작성했던 Compose Preview 글에서처럼 활용하고 있어 더 편하게 활용하고 있다.

안드로이드 Compose Preview를 잘 활용하는 방법은? - 함수를 잘 분리하자.


ViewModel에서 Clean Architecture는?

개인적으로 딱히 필요하다고 생각지 않는다. 안드로이드 자체는 서버에서 제공하는 데이터를 전달받아, 활용한다. 그래서 개인적으론 UseCase는 제외하고, Entity 정도로 변환해 이를 활용하고 있다.

XML 형태에서는 아래와 같이 활용한다.

  • Repository에서 local/remote를 가져온다.
    • remote인 경우 data class XXXResponse를 정의한다.
    • local인 경우 room은 entity를 정의한다.
  • Repository에서 ViewModel로 전달되는 데이터는 convert Entity를 한다.
  • ViewModel에서 View와의 통신에서는 XXXItem을 정의하여 던진다.
    • 간혹 별다른 가공이 필요치 않다면 XXXEntity를 그대로 View에서 활용한다.

Compose로 와서는 아래와 같다.

  • Repository에서 local/remote를 가져온다.
    • remote인 경우 data class XXXResponse를 정의한다.
    • local인 경우 room은 entity를 정의한다.
  • Repository에서 ViewModel로 전달되는 데이터는 convert Entity를 한다.
  • ViewModel에서 View와의 통신에서는 XXXItem을 정의하여 던진다.
    • XXXItem에는 @Stable 어노테이션을 추가한다.
    • XXXEntity가 View에 넘겨지지 않도록 한다.

Android 하면서 Clean Architecture를 굳이 해야 할 이유는 없다.

우린 이미 많은 패턴을 가지고 적용해 활용하고 있는데, Repository 패턴 역시 이중 하나이다. Clean Architecture는 하면 좋겠지만. 어디까지나 하면 좋은 것일 뿐이다.

하지 않는다고 이상할 이유도 없다. 대부분 단순 포워딩 하는 UseCase만 증가할 뿐이다. interface로 잘 구분한 Repository만 있어도 UnitTest 하는데 문제가 없다.

참고

Stable의 경우 문서에도 나와있지만 이 data class의 동작을 확실하게 보장하는 경우에 활용된다.

보통의 data class 전달은 달라질 부분이 없고, 새로운 데이터가 오더라도, 동작은 그대로니 말이다.

equals, hash code가 동일한 상태임을 보장한다고 보면 되겠다. 그러니 새로운 데이터가 오기 전엔 보장인 상태인데, 달라지는 경우 composition에 통지하고 업데이트하도록 한다고 한다.

@Stable을 이런 이유로 부착하고, 최적화에 도움 되어 이를 활용하고 있다.


Compose를 쓴다고 ViewModel이 달라지는가?

답은 노.

ViewModel의 용도는 동일하다. XML이라고 Compose라고 ViewModel이 달라져야 할 이유는 전혀 없다.

만약 이렇게 생각했다면 ViewModel을 왜 활용해야 하는지 이해하지 못했을 수도 있다.

분명하지만 View와 View에 대한 로직을 분리하고, ViewModel을 UnitTest 하기 위한 목적이다.

로직을 UnitTest 할 수 있는 점은 동일하다.

그렇다고 퓨어 한 ViewModel을 써야 하는가?

이 역시 답은 노!

ViewModel의 행동 패턴은 달라진 것이 없다. 원래의 목적 그대로이다.

구글 AAC-ViewModel 문서에서도 첫 줄에 나온다.

The ViewModel class is a business logic or screen level state holder.

이 말처럼 AAC-ViewModel을 쓰던 그냥 ViewModel을 쓰던 비즈니스 로직을 분리하는 목적엔 전혀 달라짐이 없다.


마무리

MVVM 따라 하기는 MVP처럼 쉽게 글이 써지지 않는 것 같다. 아무래도 기존보다 시기도 많이 지났고, Compose도 나왔으니 말이다.

뭐가 되었든 MVP든 MVVM 이든 코드를 왜 분리하려고 하는지, 이 코드를 분리하면서 얻고 싶은 목적이 무엇인지가 중요하다.

누군가는 테스트 하는 방식을 필요로 할 수도 있고, 누군가는 학습의 목적도 있을 것이다.

다음 글은 또 언젠가 작성하도록 하겠다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments