Hun's Blog

[Android] 디자인패턴 5 - MVVM패턴 본문

Android

[Android] 디자인패턴 5 - MVVM패턴

jhk-im 2020. 3. 22. 10:19

MVVM패턴이란?


참고
https://medium.com/@jsuch2362/android-%EC%97%90%EC%84%9C-mvvm-%EC%9C%BC%EB%A1%9C-%EA%B8%B4-%EC%97%AC%EC%A0%95%EC%9D%84-82494151f312

mvp패턴에서 presenter의 문제점이 있다. 
컨트롤러와 마찬가지로 시간이 지남에 따라 추가되는 비즈니스 로직이 모이게 된다. 시간이 흐르면 거대하고 다루기 어렵고 문제가 발생하기 쉽고 분리하기 어려운 presenter를 발견하게 된다. *신중한 개발자라면 앱의 변화에 맞춰 해결해 나갈 수 있다. 

- Model, View, ViewModel의 약자
- 뷰와 모델을 연결하기 위해 사용해야하는 연결 코드 감소
- MVP에서 파생된 패턴
- Microsoft에 의해 제안된 패턴

*Model : MVC, MVP와 동일하다.
*View : ViewModel에 의해 보여지는 Observable변수와 액션에 유연하게 바인딩된다.
*ViewModel : Model을 래핑하고 View에 필요한 Observable 데이터를 준비한다.
                  -> View가 Model에 이벤트를 전달하도록 Hook을 준비한다.
                  -> ViewModel은 View에 종속되지 않는다.


이미지1 MVVM 관계도


*View와 ViewModel의 관계
- ViewModel은 Model을 알지만 View를 알지 못한다.
- View는 Model을 알지 못하지만 ViewModel은 알 수 있다.
- View는 ViewModel을 관찰하고 있다가 변화가 감지되면 화면을 갱신한다.



DataBinding : View와 ViewModel 독립 

이미지 2 DataBinging 관계도


DataBinding은 View와 ViewModel 간의 데이터와 명령을 연결해주는 매개체가 되어 서로의 존재를 명확히 알지 않더라도 상호작용 할 수 있도록 도와준다.
Model에서 데이터가 변경되면 ViewModel을 거쳐 View로 전달되도록 하며 안드로이드에서는 LiveData 혹은 RxJava 등을 통해 구현할 수 있다.

*MVVM을 아무런 도움없이 구현하면 기존의 문제점을 개선하는 과정에서 새로운 문제가 발생할 수 있다. 그렇기 때문에 Databinding 이라는 툴을 이용하여 View와 ViewModel간의 의존성을 낮춰서 사용할 때 MVVM의 진가가 발휘된다는 점을 기억해두자. 



todo-mvvm-databinding 예제 

참고
https://github.com/android/architecture-samples/tree/todo-mvvm-databinding

이미지 3 todo-mvvm 구조 


빨간색 네모 부분을 먼저 이해해보기 위해 다음과 같이 구현해보았다. 

이미지 4 클래스 구조

 

이미지 5 layout

 

이미지 6 실행화면 


mvp 예제와 실행결과는 동일하다. 
이미지6에 있는 빨간색 네모 부분이 activity_main.xml 의 FrameLayout이고 아이디가 contentFrame이다. 이곳에 3가지의 프래그먼트 뷰를 버튼을 활용해 교체해주는 것이다. 현재는 Frament1만 구현되어있는 상태이다. 프래그먼트가 액티비티에 표시되기 까지 ViewModel과 Databinding이 어떻게 활용되는지 살펴보자. 


 

이미지 8 MVVM SAMPLE 흐름도


간단? 하게 흐름도를 작성해보았다. 
1번부터 천천히 살펴보면서 ViewModel과 DataBinding이 어떤식으로 구성되어있는지 알아보자. 

1. Activity와 Fragment 생성

MaintActivity - onCreate

1
2
3
4
5
6
7
8
9
10
private TestFragment findOrCreateViewFragment() {
    TestFragment testFragment1 =
            (TestFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
    if(testFragment1 == null) {
        testFragment1 = TestFragment.newInstance();
        ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
                testFragment1,R.id.contentFrame);
    }
    return testFragment1;
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 


- 먼저 MainActivity가 onCreate될때 MainActivity와 TestFragment를 생성한다.
- findOrCreateViewFragment() 메소드는 TestFragment를 반환한다.
  - 생성되어 액티비티에 연결까지 마친 프래그먼트가 있다면 해당 프래그먼트를 반환한다.
  - 처음 프래그먼트를 만들 경우 프래그먼트의 newInstance() 메소드로 인스턴스를 생성한다.
  - ActivityUtils클래스의 addFragmentActivity() 메소드로 액티비티와 연결한다.

ActivityUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ActivityUtils {
    @SuppressLint("RestrictedApi")
    public static void addFragmentToActivity (@NonNull FragmentManager fragmentManager,
                                              @NonNull Fragment fragment, int frameId) {
        checkNotNull(fragmentManager);
        checkNotNull(fragment);
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.add(frameId, fragment);
        transaction.commit();
    }
 
 
    @SuppressLint("RestrictedApi")
    public static void addFragmentToActivity (@NonNull FragmentManager fragmentManager,
                                              @NonNull Fragment fragment, String tag) {
        checkNotNull(fragmentManager);
        checkNotNull(fragment);
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.add(fragment, tag);
        transaction.commit();
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 

- 두개의 메소드가 존재하는데 입력값으로 frameId를 받을 것인지 tag를 받을것인지의 차이만 있고 동작은 동일하다.
- 최초 연결시 findOrCreateViewFragment() 에서는 frameID인 int값을 넘긴다.



2. Fragment View 표시 

TestFragment - TestFragBinding

1
private TestFragBinding mTestFragBinding;
 


TestFragment - onCreateView

1
2
3
4
5
6
7
8
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                         @Nullable Bundle savedInstanceState) {
    mTestFragBinding = TestFragBinding.inflate(inflater, container, false);
    mTestFragBinding.setView(this);
    mTestFragBinding.setViewmodel(mTestsViewModel);
    View root = mTestFragBinding.getRoot();
    return root;
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 


만든적 없는 TestFragBinding이라는 객체와 그것을 활용하여 프래그먼트를 표시하는 로직이 onCreateView안에 작성되어있다. TestFragBinding은 어디서 온 객체인지를 먼저 알아야한다. 

3. 데이터바인딩 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<layout
    <data>
        <import type="android.view.View"/>
        <variable
            name="view"
 
            type="com.jroomstudio.mvvmsample.main.TestFragment" />
        <variable
            name="viewmodel"
            type="com.jroomstudio.mvvmsample.main.TestsViewModel" />
    </data>
        android:layout_width="match_parent"
        android:layout_height="match_parent">
 
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Fragment1"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
</layout>
 
 


- 프래그먼트화면 전체가 <layout></layout> 안에 표현되어있다.
- <data></data> 내부에 view와 viewmodel로 각각의 프래그먼트 와 뷰모델 클래스가 선언되어있다.
- 이렇게 등록이 완료되면 TestFragBinding객체가 자동으로 생성된다.

app - build.gradle -> adroid{}

1
2
3
dataBinding {
    enabled = true
}
 

해당 기능을 사용하려면 app의 build.gradle에 다음과같이 dataBinding을 enabled = true로 해주어야 한다.

 

dataBinding을 활용하면 어떤 차이가 있을까? 

MVP 패턴의 프래그먼트 onCreateView

1
2
3
4
5
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                         @Nullable Bundle savedInstanceState) {
    View root = inflater.inflate(R.layout.main_test_frag, container, false);
    return root;
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 


데이터바인딩에 대한 내용을 다시한번 생각해보자.

DataBinding은 View와 ViewModel 간의 데이터와 명령을 연결해주는 매개체가 되어 서로의 존재를 명확히 알지 않더라도 상호작용 할 수 있도록 도와준다.

MVP 패턴에선 해당 프래그먼트의 xml id인 main_test_frag 을 직접 기입한다. 즉, 해당 프래그먼트가 자신과 연결되는 xml을 명확하게 알고있다는 뜻이다. 
데이터바인딩을 활용했을 때에는 프래그먼트 클래스 내부에 자신과 연결 될 xml에 대해서 명확히 기입하지 않고 데이터바인딩을 통해 생성된 TestFragBinding 객체를 통해 연결을 구현한다. 

데이터 바인딩이 Veiw와 ViewModel에서 어떠한 이점이 있는지는 아직 명확하게 파악되지 않는다. 다만 서로의 존재를 명확히 알지 않더라도 상호작용하게 한다는 내용은 어느정도 이해가 되는 것 같다. 그 점을 기억해두고 차후에 기능이 추가될 때 조금 더 시야를 넓혀보도록 하겠다. 



4. Activity와 Fragment 연결 
이미지 8의 4번과 같이 최종적으로 액티비티에 프래그먼트가 표시된다.
ViewModel이 어떻게 셋팅되는지 알아보자. 



5. ViewModel 생성 

MainActivity - onCreate

1
mViewModel = findOrCreateViewModel();
 

- 최초에 프래그먼트가 생성되고 바로 이어서 뷰모델 생성을 위해 findCreateViewModel()가 실해된다.

MainActivity - findOrCreateViewModel()

1
2
3
4
5
6
7
8
private TestsViewModel findOrCreateViewModel(){
    @SuppressWarnings("unchecked")
    ViewModelHolder<TestsViewModel> retainedViewModel =
            (ViewModelHolder<TestsViewModel>) getSupportFragmentManager()
            .findFragmentByTag(TESTS_VIEWMODEL_TAG);
    
    ...
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 

- 메소드 최초에 프래그먼트를 상속받은 ViewModelHolder<>로 프래그먼트를 생성한다. 

ViewModelHodler<VM>

1
public class ViewModelHolder<VM> extends Fragment { ... }
 

- ViewModelHolder는 <VM>으로 제네릭을 특정하지 않은 클래스이다.
- VM은 ViewModel을 의미하며 ViewModel을 제네릭으로 지정하여 사용하게 된다.

* MainActivity에서는 아직 ViewModel 객체를 생성하지 않았다. 프래그먼트 생성때와 마찬가지로 생성되어 있을때와 최초 생성할때를 구분하여 메소드가 실행된다.



6.ViewModel과 View 연결 

MainActivity - findOrCreateViewModel()

1
2
3
4
5
6
7
8
9
10
...
 
if(retainedViewModel != null && retainedViewModel.getViewModel() != null){
    return retainedViewModel.getViewModel();
}else{
    TestsViewModel viewModel = new TestsViewModel(getApplicationContext());
    ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
            ViewModelHolder.createContainer(viewModel),TESTS_VIEWMODEL_TAG);
    return viewModel;
}
 
 

이미지8의 순서에 맞게 우선 최초로 ViewModel이 생성되는 부분을 보도록 하겠다. 위의 코드에서는 else{ } 안에있는 코드가 이에 해당된다. 

- TestsViewModel() 생성자로 ViewModel 객체를 생성한다.
- ActivityUtils의 addFragmentToActivity() 메소드중 String TAG가 입력되는 메소드가 실행된다.

addFragmentToActivity()는 FragmentManager와 Fragment 그리고 frameId or tag 를 입력받는다.  Fragment를 입력하기위해 사용된 메소드를 자세히 봐보자. 



7. 최초의 연결
ViewModelHolder -> createContainer()

1
2
3
4
5
public static <M> ViewModelHolder createContainer(@NonNull M viewModel){
    ViewModelHolder<M> viewModelContainer = new ViewModelHolder<>();
    viewModelContainer.setViewModel(viewModel);
    return viewModelContainer;
}
 

- <M>이라는 지정하지 않는 제네릭을 반환하는 메소드이다.
- M은 입력받은 ViewModel이다.
- 내부에 있는 setViewModel메소드로 멤버변수인 mViewModel과 입력받은 ViewModel을 매치시킨다.
- 결과적으로 ViewModelHolder<ViewModel>을 반환한다.



8. ViewModel 접근 
ViewModelHolder -> getViewModel

1
public VM getViewModel() { return mViewModel; }
 

- 위 과정을 거쳤다면 getViewModel() 메소드로 viewModel에 접근할 수 있다.



MainActivity - 프래그먼트 뷰모델 연결 

1
testFragment1.setViewModel(mViewModel);
 

- 최종적으로 프래그먼트와 viewModel연결


정리 -
View와 ViewModel이 어떻게 분리되고 그 사이에서 DataBinding이 어떤 역할을 하는지 아주아주 얕게 알게된것 같다. mvp와 마찬가지로 기능이 추가하면서 계속해서 이해를 높여가는 방향으로 가야할 것 같다. 디자인 패턴은 이쯤에서 마무리하고 MVP로 프로젝트를 구현한 후에 프로젝트 크기가 많이 크지 않을 때 MVVM으로 변경하면서 공부해봐야겠다.