Android Clean architecture에서 LiveData는 어디에 둬야할까

최혜성·2024년 3월 28일
0

MVVM이란?

Model - View - ViewModel의 약자로 기존 앱개발 방식인 MVC를 대체하기 위한 구조로써 사용된다.
MVC에서 Controller, MVP에서 Presenter의 역할을 ViewModel이 담당하여 View와 Model간의 결합을 획기적으로 낮출 수 있다.

위 패턴과 함께, 엔티티 - 레포지토리 - 유스케이스 - 프레젠터 - UI 수준의 레이어를 통해 로직들을 분리, 위임하여 처리하게 해 유연한 개발을 추구하는 Clean Architecture라는 개념이 있다.
엔티티 - 뷰모델 형식으로 다이렉트로 접근시 엔티티의 일관성 위반은 둘째치고 엔티티가 조금이라도 바뀌면 뷰모델은 물론 모든 코드가 변경될 소지가 있다.

따라서 뷰모델은 유스케이스에게, 유스케이스는 레포지토리에게.. 식으로 위임하도록 처리해서 최대한 변경사항의 전파가 크지 않도록 개발할 수 있는 좋은 방식이다.

동영상을 가져오는 Provider를 유튜브에서 넷플릭스로 변경한다고 가정했을때 Clean Architecture에서는 넷플릭스 레포지토리를 만들고, 유스케이스에서 해당 넷플릭스 레포지토리를 접근하도록 하면 수정이 끝난다.
위 아키텍처를 적용하지 않는 예제는 여백이 부족하여 서술하지 않겠다.🤣

왜 사용하는가?

기존 MVC의 경우는 Controller가 사실상 Actitvty가 담당하는데 각종 로직을 여기다가 다 선언한다면?
웹 요청도 받아와야 하고~ 로그인 valid도 해야하고~ 파일 저장도 요청해야하고
이러면 코드가 상당히 복잡해지고 지나가던 개발자 하나 붙잡고 이해해보라 해도 못하게 된다.
그러니까 모델이랑 뷰에 대한 컨트롤러의 결합도가 본드로 붙인 수준이 되었다고 볼 수 있다

그래서 이걸 해결하기 위해 MVP를 도입해서 Presenter가 로직을 처리하고 뷰를 갱신하라고 했으나, 이번엔 뷰와 결합도가 너무 높았다.
매번 뷰에 요청을 해야했으니 어쩔 수 없었다.

본론

이런 MVVM 방식은 결합도가 낮아 유연하게 개발이 가능했다.
하지만 너무 유연해서 MVC방식에서는 간단하게 액티비티에 추가하면 될 코드를 여기선 어디다 둬야할지 생각하기 까다로운것들이 한두가지가 아니였다.

  • GetAllImagesUsecase
suspend fun loadImages() : List<Image> {
    return imageRepo.loadImages()
}

이런식으로 유스케이스가 레포지토리로부터 엔티티를 불러오는 과정을 나타낼 수 있다.
그러면 해당 유스케이스는 어디서 써야할까

  • GalleryVM
fun loadImage() {
     CoroutineScope(Dispatchers.IO).launch {
         getAllImageUsecase.loadImages()
     }
}

뷰모델에서 해당 유스케이스의 함수를 호출해 데이터를 불러올 수 있다.
근데, 불러온다 해도 뷰에 나타내야 하는데 어떻게 해야할까?

LiveData

해당 클래스를 통해 뷰의 갱신을 요청할 수 있다.
LiveData<Class>
의 형태로 사용하며 public으로 변수를 선언해 View(Activity, Fragment)에서 해당 변수에 접근하고, observe 메소드를 통해 해당 LiveData의 변화가 발생했을때 뷰를 갱신하도록 한다.

이렇게 좋은걸 도대체 어디다 둬야할까?
처음 MVVM을 배웠을 당시 Usecase에 이를 선언해두고 사용했다.

 fun loadImages() : LiveData<List<Image>> {
    return MutableLiveData(imageRepo.loadImages())
}

VM

val images = getAllImageUsecase.loadImages()

이런식으로 바로 변수에 할당해서 observe하는 방식으로 구현했었다.
나 혼자 뿌듯해하며 잘 쓰고 있었는데 문제는 새로고침을 할때였다.

images.observe(this) {image ->
image.forEachIndexed { idx, img ->
   viewImage[idx].setDrawable(img)
}

약간 이런느낌으로 구현했는데 이 방식은 새로고침을 할 수 없었다.
만약 새로고침을 할려고 usecase를 다시 호출하면 새로 불러온 결과는 새로 생성된 LiveData에 할당되어 이를 observe하지 않는 이상 값을 받아올 수 없었다.

fun refresh() {
    getAllImageUsecase.loadImages() -> LiveData 새로 생성, 즉 ignore됨
}

음..

그래서 한참을 생각해본결과 LiveData를 재활용할 수 있도록 변경하고, 새로고침 메소드를 하나 만들기로 했다.

val imageLiveData = MutableLiveData<List<Image>>
 fun loadImages() : LiveData<List<Image>> {
     refreshImage()
    return imageLiveData
}

fun refreshImage() {
    imageLiveData.value = imageRepo.loadImage()
}

그래서 뷰에서 최초로 loadImage함수를 이용, observe를 수행하는것 까지는 동일하지만, 추후 갱신이 필요할때 loadAllImageUsecase.refreshImage를 호출하면 새로고침이 수행되도록 했다

조큼.. 안이뻐요

근데 위 방식은 refresh라는 '기능'을 추가한것이다. 뭔가 하나의 유스케이스가 2가지 기능을 수행하는것 같다는 느낌을 받아 깔끔하지 못했다.
당장 위 유스케이스만 봐도 로드만 하고 끝나는게 아니라 새로고침이라는 하나의 기능을 더 수행하니.

그래서 livedata를 usecase 밖으로 빼냈다.
VM

val images = MutableLiveData<List<Image>>

fun loadImages() {
    images.value = loadAllImageUsecase.loadImage()
}

이렇게 하면 최초 observe를 수행하고, view에서 갱신이 필요할때 vm의 loadImages라는 함수를 호출하기만 해도 갱신된값을 받을 수 있다.

따라서, refresh라는 메소드를 usecase에 구현할 필요가 없으므로 유스케이스는 자신의 한가지 일인 이미지 불러오기만 잘 하면 된다!

하지만 iamges를 observe했을때 바로 값을 가져오는것이 아니기 때문에 onCreate에서 loadImages함수를 호출할 필요가 생겼다. 혹은 VM init시 각 유스케이스 한번씩 호출해도 될것 같구

항상 최적의 결과만을 도출하는 방법은 없으니, 제일 효율적인 방법을 택하는것이 좋아보인다.

https://heegs.tistory.com/m/58
해당 블로그에서 정보를 많이 얻었다. 코루틴을 사용하지 않아 Flow를 이용했으나,
실제 데이터를 받을때 VM의 livedata값을 변경한다.

profile
KRW 채굴기

0개의 댓글