-
그래서 MVC, MVP, MVVM 디자인패턴 이 뭔데? - 안드로이드 /뿌시레기/Android 2020. 6. 1. 17:11
tl;dr
MVC (Model - View - Controller) Model 은 데이터, View 는 XML 파일, Controller 는 Activity MVP (Model - View - Presenter) Model 은 데이터, View 는 Activity, Presenter 는 Model 과 View 를 연결해주는 매개체 MVVM (Model - View - ViewModel) Model 은 데이터, View 는 Activity, ViewModel 은 Model 과 View 를 연결해주는 매개체 이번 블로그 포스팅에 사용한 소스코드는 Github 에서 확인할 수 있다. (tutorialapp/designpattern)
서론
이번에는 디자인 패턴인 MVC, MVP, MVVM 에 대해서 설명해 보려고 한다.
항상 혼자서 개발하다 보면 이런 생각에 빠지곤 한다. "내가 짜고 있는 코드가 적절한 것인가?", "남들은 어떻게 짜고 어떻게 만들고 있을까?" , "더 나은 방법으로 제품을 만들고 싶은데 잘 모르겠네..." 이런 궁금증들을 풀기 위해 실무에서 디자인 패턴을 어떻게 접목시키고 있는지 알아보려고 한다.
처음 프로그래밍을 접한 사람들은 내가 만든 코드가 빌드되고 내가 원하는 대로 돌아가는 것에 커다란 희열감(?)을 느낀다. 하지만 바로 고통이 뒤따르게 되는데, 만약 내가 만든 코드에 예상치 못한 문제가 생겼을 때 어느 부분이 문제인지 찾기 더럽게 힘들고 힘들게 고친다 하더라도 이후에 다른 이슈가 생기게 되면 이런 X같은 상황이 무한 반복된다.
개인이 하는 프로젝트면 어느정도 예상되는 부분을 찾아서 고쳐나갈 수 있겠지만, 여러 사람과 협업해야 되는 상황, 큰 프로젝트를 관리해야 되는 상황에 맞딱뜨리게 되면 위와 같은 행위를 반복할 수 있을까? 정답은 [아니다] 일 것이다. 그렇다면 현업에서는 이러한 부분을 해결하기 위해 어떤 방식으로 코딩을 짤까?
이런 의문점이 들어 구글링을 하다보면 design pattern, mvc, mvp, mvvm, mvi 와 같은 키워드가 눈에 보일 것이다.
공통 용어
Model : 간단하게 말해서 데이터, 상태, 비즈니스 로직 이다. 프로젝트 내에서 쓰이는 데이터를 저장하고 또는 가공, 처리하는 역할을 한다. View 와 (Controller, Presenter, View Model) 에 의존적이지 않으므로 재사용할 수 있다.
더보기public class DPModel { private int x, y; private boolean isPlaying; private boolean isFinished; public DPModel() { isPlaying = false; isFinished = true; } public void start() { isPlaying = true; isFinished = false; } public void stop() { isPlaying = false; isFinished = false; } public void reset() { isPlaying = false; isFinished = true; } public void move(int x, int y) { if(!isFinished) { this.x = x; this.y = y; } } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } public boolean isPlaying() { return isPlaying; } public void setPlaying(boolean playing) { isPlaying = playing; } public boolean isFinished() { return isFinished; } public void setFinished(boolean finished) { isFinished = finished; } }
View : 간단하게 말해서 UI(User Interface) 이다. 다만 각 디자인 패턴에 따라 그 용도에 차이가 있다. 이 부분은 밑에서 자세히 설명하도록 한다.
MVC ( Model - View - Controller )
View : MVC 에서의 View 는 비교적 아무 역할도 없는 느낌이다. 그냥 여기에 이런 뷰가 있다 정도를 표현한다고 생각하면 된다. 뷰를 어떻게 표현하는지에 따라 난이도의 차이가 있긴 하겠지만 단순하게 XML 파일이라고 생각하면 된다.
더보기<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".designpattern.mvc.MvcActivity"> <include layout="@layout/activity_d_p_tv"/> <include layout="@layout/activity_d_p_linear"/> <include layout="@layout/activity_d_p_view"/> </RelativeLayout>
Controller : 어플리케이션이 실행하게 되면 작동하는 컨트롤러이다. Model 과 View 를 서로 연결해주는 역할을 하고 유저에게 액션을 받아 처리하는 역할까지 맡게 된다. 단순하게 유저의 액션을 받아서 처리할 수 있는 액티비티, 프래그먼트라고 생각하면 된다.
더보기@SuppressLint("ClickableViewAccessibility") public class MvcActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener { public static final String TAG = MvcActivity.class.getSimpleName(); private DPModel model; private TextView tv; private Button start, stop, reset; private RelativeLayout view; private View square_view; private int centerX, centerY; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_mvc); tv = (TextView) findViewById(R.id.d_p_tv); start = (Button) findViewById(R.id.d_p_linear_start); stop = (Button) findViewById(R.id.d_p_linear_stop); reset = (Button) findViewById(R.id.d_p_linear_reset); view = (RelativeLayout) findViewById(R.id.d_p_view); square_view = (View) findViewById(R.id.d_p_square_view); start.setOnClickListener(this); stop.setOnClickListener(this); reset.setOnClickListener(this); view.setOnTouchListener(this); model = new DPModel(); startVISIBLE(); setXYTextInit(); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.d_p_linear_start: start(); break; case R.id.d_p_linear_stop: stop(); break; case R.id.d_p_linear_reset: reset(); break; default: break; } } @Override public boolean onTouch(View v, MotionEvent event) { if(v.getId() == R.id.d_p_view) { if((event.getX() >= 0 && event.getX() <= view.getWidth() - square_view.getWidth()) && (event.getY() >= 0 && event.getY() <= view.getHeight() - square_view.getHeight()) && model.isPlaying()) { move(event); return true; } } return true; } private void start() { getCenter(); model.start(); stopVISIBLE(); } private void stop() { model.stop(); startVISIBLE(); } private void reset() { if(!model.isFinished()) { moveCenter(); setXYTextInit(); model.reset(); startVISIBLE(); } } private void move(MotionEvent event) { model.move((int) event.getX(), (int) event.getY()); moveSquare(); setXYText(model.getX(), model.getY()); } private void moveCenter() { model.move(centerX, centerY); Log.d("work??", "x : " + centerX); Log.d("work??", "y : " + centerY); moveSquare(); } private void moveSquare() { square_view.setX((float) model.getX()); square_view.setY((float) model.getY()); } private void getCenter() { centerX = (int) square_view.getX(); centerY = (int) square_view.getY(); } @SuppressLint("SetTextI18n") private void setXYTextInit() { tv.setText("START를 눌러주세요"); } @SuppressLint("SetTextI18n") private void setXYText(int x, int y) { tv.setText("X : " + x + ", Y : " + y); } private void startVISIBLE() { start.setVisibility(View.VISIBLE); stop.setVisibility(View.GONE); } private void stopVISIBLE() { start.setVisibility(View.GONE); stop.setVisibility(View.VISIBLE); } }
MVC 에 대한 생각
생각 보다 기존에 본인이 작성한 코드와 별반 다르게 없다고 생각할 수 있다. MVC 의 이점이라고 하면, 완벽하게 모델과 뷰를 분리해준다는 점, 모델을 쉽게 테스트 할 수 있다는 점을 들 수 있다. (뷰는 단순한 XML 파일이기 때문에 테스트할 것이 거의 없다)
이와 대비해서 문제점도 확연하다. MVC 를 사용하게 되면, 컨트롤러가 안드로이드에 깊게 종속되므로 컨트롤러를 테스트하는데 문제가 있다. 또한 프로젝트를 수정하거나 새로운 기능을 추가할 때, 많은 코드가 전부 컨트롤러 즉 액티비티 및 프래그먼트에 모이게 되고 비대해 지게 되면서 유지보수에 어려움이 따른다.
MVP ( Model - View - Presenter )
View : MVP 에서의 View 는 MVC 에서 Controller 에 있던 액티비티와 프래그먼트가 넘어왔다고 생각하면 된다. 또한 뷰를 관리해주는 인터페이스를 추가하여 Presenter 를 독립적으로 만들어준다.
더보기@SuppressLint("ClickableViewAccessibility") public class MvpActivity extends AppCompatActivity implements MvpView, View.OnClickListener, View.OnTouchListener { public static final String TAG = MvpActivity.class.getSimpleName(); private TextView tv; private Button start, stop, reset; private RelativeLayout view; private View square_view; private int centerX, centerY; private MvpPresenter mvpPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_mvp); tv = (TextView) findViewById(R.id.d_p_tv); start = (Button) findViewById(R.id.d_p_linear_start); stop = (Button) findViewById(R.id.d_p_linear_stop); reset = (Button) findViewById(R.id.d_p_linear_reset); view = (RelativeLayout) findViewById(R.id.d_p_view); square_view = (View) findViewById(R.id.d_p_square_view); start.setOnClickListener(this); stop.setOnClickListener(this); reset.setOnClickListener(this); view.setOnTouchListener(this); mvpPresenter = new MvpPresenter(this); mvpPresenter.onCreate(); } @Override protected void onResume() { super.onResume(); mvpPresenter.onResume(); } @Override protected void onPause() { super.onPause(); mvpPresenter.onPause(); } @Override protected void onDestroy() { super.onDestroy(); mvpPresenter.onDestroy(); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.d_p_linear_start: mvpPresenter.start(); break; case R.id.d_p_linear_stop: mvpPresenter.stop(); break; case R.id.d_p_linear_reset: mvpPresenter.reset(centerX, centerY); break; default: break; } } @Override public boolean onTouch(View v, MotionEvent event) { if(v.getId() == R.id.d_p_view) { if((event.getX() >= 0 && event.getX() <= view.getWidth() - square_view.getWidth()) && (event.getY() >= 0 && event.getY() <= view.getHeight() - square_view.getHeight())) { mvpPresenter.move((int) event.getX(), (int) event.getY()); return true; } } return true; } @Override public void getCenter() { centerX = (int) square_view.getX(); centerY = (int) square_view.getY(); } @Override public void setPosition(int x, int y) { square_view.setX((float) x); square_view.setY((float) y); } @SuppressLint("SetTextI18n") @Override public void setXYTextInit() { tv.setText("START를 눌러주세요"); } @SuppressLint("SetTextI18n") @Override public void setXYText(int x, int y) { tv.setText("X : " + x + ", Y : " + y); } @Override public void startVISIBLE() { start.setVisibility(View.VISIBLE); stop.setVisibility(View.GONE); } @Override public void stopVISIBLE() { start.setVisibility(View.GONE); stop.setVisibility(View.VISIBLE); } }
public interface MvpView { void getCenter(); void setPosition(int x, int y); void setXYTextInit(); void setXYText(int x, int y); void startVISIBLE(); void stopVISIBLE(); }
Presenter : 기본적으로 Controller 와 같은 역할을 한다고 생각하면 된다. Controller 와 다른점이 있다면 뷰에 연결되는 것이 아니라 단순한 인터페이스라는 점이다. 이를통해 테스트 용이성 뿐만 아니라 모듈화/유연성 문제를 해결할 수 있다. 다만 한가지 제약사항이 있는데 그 어떤 안드로이드 API 도 참조해선 안된다. 이를 지키지 않는다고 큰 문제가 있지는 않지만 되도록 해당 제약사항을 따르는 방향으로 구성하는 편이 좋다.
더보기public class MvpPresenter implements Presenter { public static final String TAG = MvpPresenter.class.getSimpleName(); private MvpView view; private DPModel model; MvpPresenter(MvpView view) { this.view = view; this.model = new DPModel(); } // 라이프 사이클 public void onCreate() { model = new DPModel(); view.startVISIBLE(); view.setXYTextInit(); } public void onPause() { } public void onResume() { } public void onDestroy() { } // 사각형 이동 관련 함수 @Override public void start() { view.getCenter(); model.start(); view.stopVISIBLE(); } @Override public void stop() { model.stop(); view.startVISIBLE(); } @Override public void reset(int centerX, int centerY) { if(!model.isFinished()) { moveCenter(centerX, centerY); view.setXYTextInit(); model.reset(); view.startVISIBLE(); } } @Override public void move(int x , int y) { if(model.isPlaying()) { model.move(x, y); moveSquare(); view.setXYText(model.getX(), model.getY()); } } @Override public void moveCenter(int centerX, int centerY) { model.move(centerX, centerY); moveSquare(); } @Override public void moveSquare() { view.setPosition(model.getX(), model.getY()); } }
public interface Presenter { void start(); void stop(); void reset(int centerX, int centerY); void move(int x, int y); void moveCenter(int centerX, int centerY); void moveSquare(); }
MVP 에 대한 생각
MVC 와 비교해보면 꽤나 코드가 깔끔해 졌다는 것을 구성만 보고도 알 수 있을 것이다. 이점이라고 하면, 뷰와 프레젠터가 1 : 1 대응으로 View 인터페이스를 구현했다면 Presenter 로직을 손 쉽게 테스트 할 수 있다.
다만 MVP 에도 문제점이 있는데, MVC 와 마찬가지로 프로젝트를 수정하거나 새로운 기능을 추가할 때, 많은 프리젠테이션 로직이 프레젠터에 모이게 되고 비대해 지게 되면서 유지보수에 어려움이 따른다.
MVVM ( Model - View - View Model )
Model : MVC, MVP 와 동일하다. 다만, 기존 모델에 비즈니스 로직을 추가해주기 위해 MVVM 에만 SquareParams 라는 모델을 추가해 주었다.
더보기public class SquareParams { private int x, y, w, h; public SquareParams() {}; public SquareParams(int x, int y, int w, int h) { this.x = x; this.y = y; this.w = w; this.h = h; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } public int getW() { return w; } public void setW(int w) { this.w = w; } public int getH() { return h; } public void setH(int h) { this.h = h; } }
View : MVVM 에서의 View 는 MVC 에서 Controller 에 있던 액티비티와 프래그먼트가 넘어왔다고 생각하면 된다. 또한 가지 다른점은 데이터 바인딩을 위해 XML 을 적절히 변경해 주어야 한다. 추가로 build gradle(:app) 파일에 밑에 데이터 바인딩을 위한 코드를 집어넣어 주어야 한다. 이를 통해 뷰는 뷰모델에 의해 모델과 유연한 바인딩이 가능하게 된다.
dataBinding { enabled = true }
더보기<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <import type="android.view.View" /> <variable name="viewModel" type="com.kwang0.tutorialapp.designpattern.mvvm.MvvmViewModel" /> </data> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".designpattern.mvvm.MvvmActivity"> <TextView android:id="@+id/mvvm_tv" android:layout_width="match_parent" android:layout_height="64dp" android:textSize="@dimen/textSmall" android:textColor="@color/colorBlack" android:padding="@dimen/bigPadding" android:gravity="start|center" app:stringResult="@{viewModel.model}"/> <LinearLayout android:id="@+id/mvvm_linear" android:layout_width="match_parent" android:layout_height="48dp" android:orientation="horizontal" android:layout_below="@+id/mvvm_tv"> <Button android:id="@+id/mvvm_linear_start" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/d_p_start" android:onClick="@{() -> viewModel.start(mvvmSquareView)}" android:visibility="@{viewModel.model.playing ? View.GONE : View.VISIBLE}"/> <Button android:id="@+id/mvvm_linear_stop" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/d_p_stop" android:onClick="@{() -> viewModel.stop()}" android:visibility="@{viewModel.model.playing ? View.VISIBLE : View.GONE}"/> <Button android:id="@+id/mvvm_linear_reset" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/d_p_reset" android:onClick="@{() -> viewModel.reset()}"/> </LinearLayout> <RelativeLayout android:id="@+id/mvvm_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@+id/mvvm_linear" android:clickable="true" android:focusable="true" android:background="@color/colorPrimaryDark" app:touchListener="@{viewModel.onTouchListener}"> <View android:id="@+id/mvvm_square_view" android:layout_width="16dp" android:layout_height="16dp" android:layout_centerInParent="true" android:background="@color/colorAccent" app:moveSquare="@{viewModel.model}"/> </RelativeLayout> </RelativeLayout> </layout>
View Model : 뷰모델은 기본적으로 뷰에 종속되지 않는다. 다르게 말하면 종속시키면 안 된다. 뷰모델을 통해 사용할 모델을 래핑하고 뷰에 바인딩 시켜줄 옵져버블 데이터를 생성한다. 또한 뷰가 모델에 이벤트를 잘 전달할 수 있도록 BindingAdapter 을 준비한다.
더보기@SuppressLint("ClickableViewAccessibility") public class MvvmViewModel extends BaseObservable implements ViewModel { public static final String TAG = MvvmViewModel.class.getSimpleName(); @Bindable public DPModel model = null; public int centerX = 0, centerY = 0; @Bindable public SquareParams squareParams = null; public MvvmViewModel() { this.model = new DPModel(); this.squareParams = new SquareParams(-1, -1, -1, -1); } // Here we implement delegate methods for the standard Android Activity Lifecycle. // These methods are defined in the Presenter interface that we are implementing. public void onCreate() { } public void onPause() { } public void onResume() { } public void onDestroy() { } @Override public void start(View view) { setParams(view); getCenter(); model.start(); setOnTouchListener(moveTouchListener); notifyPropertyChanged(BR.model); } @Override public void stop() { model.stop(); setOnTouchListener(emptyTouchListener); notifyPropertyChanged(BR.model); } @Override public void reset() { if(!model.isFinished()) { moveCenter(centerX, centerY); model.reset(); setOnTouchListener(emptyTouchListener); notifyPropertyChanged(BR.model); } } private void setParams(View view) { if(squareParams.getX() == -1 && squareParams.getY() == -1) { squareParams.setX((int) view.getX()); squareParams.setY((int) view.getY()); squareParams.setW(view.getWidth()); squareParams.setH(view.getHeight()); notifyPropertyChanged(BR.squareParams); model.setX(squareParams.getX()); model.setY(squareParams.getY()); } } private void getCenter() { centerX = (int) squareParams.getX(); centerY = (int) squareParams.getY(); } private void moveCenter(int centerX, int centerY) { model.move(centerX, centerY); notifyPropertyChanged(BR.model); } @Bindable public View.OnTouchListener onTouchListener; private View.OnTouchListener emptyTouchListener = null; private View.OnTouchListener moveTouchListener = new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if((event.getX() >= 0 && event.getX() <= v.getWidth() - squareParams.getW()) && (event.getY() >= 0 && event.getY() <= v.getHeight() - squareParams.getH())) { if(model.isPlaying()) { model.move((int) event.getX(), (int) event.getY()); } notifyPropertyChanged(BR.model); return true; } return true; } }; private void setOnTouchListener(View.OnTouchListener onTouchListener) { this.onTouchListener = onTouchListener; notifyPropertyChanged(BR.onTouchListener); } }
@SuppressLint("ClickableViewAccessibility") public class MvvmBindingAdapter { public static final String TAG = MvvmBindingAdapter.class.getSimpleName(); @BindingAdapter({"touchListener"}) public static void setTouchListener(View self, View.OnTouchListener onTouchListener){ if (onTouchListener != null) self.setOnTouchListener(onTouchListener); } @BindingAdapter("moveSquare") public static void setSquareMove(View self, DPModel model) { self.setX((int) model.getX()); self.setY((int) model.getY()); } @SuppressLint("SetTextI18n") @BindingAdapter("stringResult") public static void getResult(TextView self, DPModel model) { if(model.isPlaying()) { self.setText("X : " + model.getX() + ", Y : " + model.getY()); } else { self.setText("START를 눌러주세요"); } } }
public interface ViewModel { void start(View view); void stop(); void reset(); }
MVVM 에 대한 생각
MVVM 을 사용하기 위해선 기본적인 안드로이드 구조와 MVVM 자체의 데이터 바인딩에 대한 이해도가 필요하다고 생각한다. 이점이라고 하면, 당연히 뷰에 대한 의존도가 전혀 없으므로 MVP 처럼 가상의 뷰를 만들 필요도 없이 데이터 변화를 추적하면서 테스트할 수 있다. 또한 ViewModel 과 View 의 종속성이 1 : n 관계이므로 그만큼 코드의 양을 줄일 수 있다고 볼 수 있다.
MVVM 에도 문제점이 있는데, 프로젝트를 수정하거나 새로운 기능을 추가할 때, 추가되는 프리젠테이션 로직을 처리하기 위해 XML 이나 View Model, BindingAdapter 에 코드를 추가하게 되고 비대해 지게 되면서 유지보수에 어려움이 따른다.
결론
MVC, MVP, MVVM 디자인 패턴에 알아보았다.
MVC, MVP, MVVM 패턴을 사용하는 것은 보인의 자유고 선택이다. 무엇이 무엇보다 더 좋다라고 하기에는 무리가 있지 않을까? 혹자는 "MVP 가 MVC 보다 좋고 MVVM 이 MVP 보다 좋다" 라고 말할 수 있겠지만, 혼자 개발하면서 페이지수가 2개인 앱을 만드는데 굳이 MVP, MVVM 를 이용해서 만들 필요는 없다고 생각한다.
그리고 MVP 가 좋은 상황이 있는 것이고 MVVM 을 사용하는게 좋은 상황이 있다고 생각된다. 물론 똑같은 뷰모델을 여러 뷰에서 사용하기 위해서는 당연히 MVVM 이 좋겠지만, 그렇지 않은 부분에서 까지도 MVVM 을 이용해서 코드를 짠다면 MVP 랑 다를 것이 없으니깐 말이다.
이후에는 MVC 패턴은 간단하니 제쳐두고 MVP, MVVM 패턴을 어떻게 활용하는지도 알아보도록 하자
내용이 잘 정리 되었는지 모르겠지만 이번 포스팅을 통해서 많은 사람들이 디자인 패턴에 대해 이해할 수 있고 지금껏 갖고 있던 궁금증을 풀 수 있었으면 좋겠다.
참조
https://academy.realm.io/kr/posts/eric-maxwell-mvc-mvp-and-mvvm-on-android/
https://github.com/ericmaxwell2003/ticTacToe
https://medium.com/@jsuch2362/android-mvvm-%EC%9D%84-%EC%9C%84%ED%95%9C-databinding-34cd9be44c63
'Android' 카테고리의 다른 글
[Android] LinearLayout 기본부터 다지고 쀼셔버리기 (0) 2021.01.09 [Android] FrameLayout 기본부터 다지고 부셔버리기 (0) 2021.01.09 xml 디자인(Design)탭 알아보기 - 안드로이드 /뿌시레기/ (0) 2019.11.18 [android] 프로젝트 내비게이션 변경하기(android -> project) //Wello Horld// (0) 2019.11.15 [android] 패키지 묶기(Compact Middle Packages) //Wello Horld// (0) 2019.11.15