최데브는 오늘도 프로그래밍을 한다.

MVVM 패턴 설명 - 2(view Model) 본문

Android

MVVM 패턴 설명 - 2(view Model)

최데브 2021. 3. 27. 16:05
반응형

정말 오랜만에 MVVM 패턴의 두번째 글을 쓴다.

 

그동안 이것저것 바빴는데 다 핑계처럼 들릴거 같아서 그냥 포스팅이나 하겠다.

사실 핑계 맞다.

 

저번 포스팅은 View에 대해 다뤘는데 오늘은 ViewModel 에 대해서 적으려고 한다.

 

ViewModel은 MVP 패턴에 프레젠터랑 비슷한 느낌이다.

중간에서 데이터를 받고 전달해주는 우편부 역할.

그러나 다른점이 있으니 패턴이름도 달라졌을터

 

의존성의 문제가 달라졌다.

MVP 패턴의 프레젠터는 모델과 뷰에 각각 의존성을 가지고 있었는데

MVVM 은 data Binding 이라는 개념을 통해 Model 파트와의 의존성만 가지고

view와의 의존성은 가지지 않는 구조를 갖게 되었다.

 

이해가 되지 않는다면 인터넷에 MVVM 패턴이라고만 쳐도 그림으로 그려져 있는것을 볼 수 있을 것이다.

그렇게 보고나면 확 와닿는다.

 

다시 정리하자면 View Model은 

view로의 의존성이 없어졌으며 Model 에 데이터를 요청하여 받은 데이터를 가공하고

Data Binding을 통해 view에서 데이터가 갱신되어 보이도록 하는 역할을 가지고 있다.

 

자 일단 내가 다른 코드를 참고하여 만든 코드부터 보자.

 

//View 가 참조할 ViewModel
//다른 ViewModel 들은 이 BaseViewModel 을 상속받는다.
open class BaseKotlinViewModel : ViewModel() {
    /**
     * RxJava 의 observing을 위한 부분.
     * addDisposable을 이용하여 추가하기만 하면 된다
     */
    private val compositeDisposable = CompositeDisposable()
    //CompositeDisposable() 은 RxJava에서 Disposable 을 손쉽게 관리하기 위해사용
    //Disposable 은 Observable 객체에서 발행할 아이템을 정의한 후 subscribe()를 통해 스트림을 생성하고 아이템을 발행했다.
    // /이 subscribe()를 호출한 후에는 Disposable 객체가 반환된다.

    //Model 에 들어오는 Rxjava의 Observable 들은 CompositeDisposable에 추가한다.
    // Observable들을 옵저빙할때 addDisposable()을 쓰게 될 것.
    fun addDisposable(disposable: Disposable) {
        compositeDisposable.add(disposable)
    }

    //그리고 ViewModel이 없어질때 비워준다.
    //ViewModel은 View 와 생명주기를 공유. View가 없어질때 ViewModel도
    //아래 함수가 호출된다.
    override fun onCleared() {
        compositeDisposable.clear()
        super.onCleared()
    }
}

 

위 코드는 모든 viewModel 들에게서 공통적으로 필요한 부분을만 따로 빼내서 만든 상위 클래스다.

나는 프로젝트를 진행할때 RxJava를 적용하면서 만들려고 했기 때문에 위와 같은 처리가 필요했다. 

일단은 이 부분은 Rxjava 파트기 때문에 넘어가겠다.

 

class MainViewModel(private val model: DataModel) : BaseKotlinViewModel() {

    private val TAG = "MainViewModel"
    //LivaDate의 값을 변경하기 위해서는 MutableLiveData 를 사용해야한다.
    private val _imageSearchResponseLiveData = MutableLiveData<ImageSearchResponse>()
    val imageSearchResponseLiveData: LiveData<ImageSearchResponse>
        get() = _imageSearchResponseLiveData
   // _imageSearchResponseLiveData 와 imageSearchResponseLiveData가 따로 있는 이유는
    //외부에서 Livedata를 변경하지 못하고, 내부에선 변경이 가능하게 하기 위함이다.
    fun getImageSearch(query: String, page:Int, size:Int) {
        addDisposable(model.getData(query, KakaoSearchSortEnum.Accuracy, page, size)// addDisposable는 Rxjava 안에 model.getData 는 retrofit2 동작
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())// observeOn 은 Observable이 다음처리를 진행할때 사용할 스레드를 지정 //https://vagabond95.me/posts/is-this-rxjava-1/ 참고
            .subscribe({
                it.run {
                    if (documents.size > 0) {
                        Log.d(TAG, "documents : $documents")
                        _imageSearchResponseLiveData.postValue(this)
                        //postValue 와 setValue의 차이는 setValue는 호출하는 당사자가
                        // UI 스레드가 아니라면 반영이 안되지만 postValue는
                        //UI 스레드로 post 해주기 때문에 반영이 된다.
                    }
                    Log.d(TAG, "meta : $meta")
                }
            }, {
                Log.d(TAG, "response error, message : ${it.message}")
            }))
    }



}

주석을 열심히 달아놨지만 MVVM 패턴의 동작 흐름에 대해서만 한번 더 적어봐야겠다.

위 MainViewModel 이라는 class 의 getImageSearch 라는 메소드가 전 포스팅에서 MainActivity 에서 실행되는것을 볼 수 있을것이다.

 

class MainActivity : BaseKotlinActivity<ActivityMainBinding, MainViewModel>() {
    override val layoutResourceId: Int
        get() = R.layout.activity_main

    override val viewModel: MainViewModel by viewModel() // Koin 으로 의존성 주입
    private val mainSearchRecyclerViewAdapter: MainSearchRecyclerViewAdapter by inject()

    override fun initStartView() {
        main_activity_search_recycler_view.run {
            adapter = mainSearchRecyclerViewAdapter
            layoutManager = StaggeredGridLayoutManager(3, 1).apply {
                gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
                orientation = StaggeredGridLayoutManager.VERTICAL
            }
            setHasFixedSize(true)
        }
    }

    override fun initDataBinding() {
        //View에서 View 모델로는 아래처럼 의존성이 있다. 그런데 observe 를 사용하기 때문에 반대로는 의존성이 없어도 된다.
        viewModel.imageSearchResponseLiveData.observe(this, Observer {
            it.documents.forEach {document ->
                mainSearchRecyclerViewAdapter.addImageItem(document.image_url, document.doc_url)
            }
            mainSearchRecyclerViewAdapter.notifyDataSetChanged()
        })
    }

    override fun initAfterBinding() {
        main_activity_search_button.setOnClickListener {
            viewModel.getImageSearch(main_activity_search_text_view.text.toString(), 1, 80)
        }
    }

}

위 코드인데 Koin 라이브러리를 통해 

override val viewModel: MainViewModel by viewModel() // Koin 으로 의존성 주입

이렇게 viewModel 이 선언된걸 볼 수 있는데 

위에서 ViewModel이 View로의 의존성을 없앴다고 적어놨지만 반대로 view에서 ViewModel로는 의존성을 가지고 있으므로 이 부분을 혼동하지 않기를 바란다.

 

다시 돌아와서 저렇게 Activity 즉 View에서 VIewModel인 MainViewModel 을 참조하고 있고

initAfterBinding 내부에서 getImageSearch 를 호출하고 있다.

그러면 다시 getImageSearch 함수내용을 한번 보자.

 

여기서는 Rxjava를 통해 retrofit 을 통해 넘어온 single 객체를 처리하고 있지만 여기서는 이게 중요한게 아니다.

MainViewModel 코드 상단에 LiveData 라는것이 보이는가.

view 에 보여주고 싶은 데이터를 postValue를 이용해서 liveData에 넣어주고 있다.

 

그리고 다시 View인 MainActivtiy 코드를 보면 initDataBinding 내부에서 viewModel의 imageSearchResponseLiveData를

obseve 하고 있는 코드를 볼 수 있다.

observe가 뭘까?

관찰하고 있다는 뜻이다. 이 LiveData는 말 그대로 살아있는? 데이터라서 값의 변동이 있을때 따로 처리를 해주지 않아도 변화한것을 관측하고 변화한 시점에 처리를 해줄수가 있어진다.

 

이렇게 진행되기 때문에 view에서 viewmodel의 livedata를 계속 관찰하고 있으므로 viewmodel에서는 mvp패턴의 프레젠터처럼 '아 지금 이 값이 바뀌었어!' 라고 따로 알려주지 않아도 자동으로 view에서 캐치하고 값을 변경시킨다.

즉 viewmodel에서 view로는 의존성을 가지지 않아도 되게 되었다는 점이 이 글이 핵심이 된다.

 

이번 포스팅은 여기까지!

어짜피 대충써도 상관없잖아.. 아무도 안읽을거잖아요.. 나만 나중에 알아보면 돼ㅠ

반응형
Comments