Hun's Blog

[Android] 디자인패턴 3 - MVP 패턴 본문

Android

[Android] 디자인패턴 3 - MVP 패턴

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

MVP패턴이란?


안드로이드에서 MVC 패턴에 대해 떠올려 보면 Model은 분리되어 있고 View와 Control이 함께 있는 구조라는 것이다. 그 예를 Activity로 들 수 있고 실제로 Activity 내부에서 View와 Control에 관련된 내용들을 모두 구현할 수 있고 그렇게 해왔다. 그러면서 Activity라는 하나의 클래스에 굉장히 많은 로직이 포함되고 코드줄이 길어진다는 문제점이 발생한다는 것도 기억해보자. 

- Model, View, Presenter의 약자
- 안드로이드에서 MVC 패턴 중 View와 Control이 함께 공존하는 문제점 해결
- 안드로이드에서 가장 많이 사용하는 패턴

*Model : Data와 관련된 처리 / MVC의 Model과 동일하다.
*View :  이벤트가 발생하면 Presenter로 알림
*Presenter : View에서 전달받은 이벤트를 처리하여 다시 View로 반환

MVP는 View와 Control이 묶이지 않도록 한다.

 

 

이미지1 MVP 동작 순서


1. View/Control: 이벤트 발생
2. View/Control: 이벤트를 Presenter에 알림
3. Presenter : 이벤트 수신하고 이벤트 형태에 맞게 Model에 데이터 요청
4. Model : Presenter의 요청 확인
5. Model : Presenter에 데이터 전달
6. Presenter : Model로부터 전송받은 데이터를 가공하여 View/Control로 전달
7. View/Control: Presenter로부터 전송받은 데이터에 맞게 UI 갱신

상황에 따라 Presenter에서 Model에 데이터를 요청하지 않을 수도 있지만 기본적인 형태는 위와 같다.

*View가 아닌 View/Control이라고 표기한 이유는 안드로이드에서 기본적으로 View와 Control이 공존한다는 것을 표현한 것이다. 지난 MVC 글에서 안드로이드 MVC의 동작순서를 보면 View/Control에서 Model에 대한 데이터 요청과 데이터 가공까지 전부 포함하고 있었으나 MVP패턴에서는 분리가되어 View/Control에서 오직 View 관련된 내용만 포함되었다는 것을 확인할 수 있다. 

더 자세한 내용은 예제를 통해서 확인하도록 하겠다.

 


뉴스앱 MVP패턴 적용 

 

 

*Model
MVC와 동일하며 변화가 없다.

*View
액티비티 혹은 프래그먼트가 오로지 View의 역할만 담당하게 되어 View/Control이 아닌 View에 관련된 내용만 표시하게 된다. View 인터페이스를 구현하여 해당 View를 담당할 Presenter에서 컨트롤하게 하는 것이다. 이렇게 되면 특정 뷰와 상관없이 가상 뷰를 구현하여 간단한 유닛테스트를 할 수 있게된다.

*Presenter
본질적으로 MVC의 컨트롤러와 같지만, 뷰에 연결되는 것이 아닌 단순히 인터페이스라는 점이 다르다. 극단적으로 MVP를 구현하는 방법은 Presenter가 절대로 어떠한 안드로이드 API나 코드가 참조되지 않도록 하는 것이다.

구분되어진 모습을 살펴보도록 하자.

이미지 2 뉴스앱 MVP 적용 


구분하기 편하도록 model, view, presenter로 패키지를 나누었다.

NewsData
동일

MainView

1
2
3
public interface MainView {
    void showNewsList();
}
 

View 인터페이스이며 해당 뷰의 액티비티에서 implements 하여 뉴스의 리스트를 보여줄 메소드를 반드시 구현하도록 강제한다. 더 많은 기능들이 추가된다면? 예를들어 리스트 새로고침이라던지 스크롤해서 새로운 데이터를 업데이트 하는 기능들이 추가된다면 refreshNewsList() 메소드가 추가될 것이다. MainView 인터페이스를 확인하면 어떠한 기능들이 구현되어있는지 한눈에 볼 수 있다.  가공된 데이터를 화면에 뿌려주는것은 view의 영역에서 해야하는 일이다.

뉴스 api에 접근해서 데이터를 받아오고 NewsData 모델을 생성해서 리스트에 차곡차곡 쌓아 가공한 후에 view로 보내주는 역할은 Presenter에서 담당하게 된다.


Presenter

1
2
3
public interface Presenter {
    void addNewsList(String title, String description, String imgUrl);
}
 
 

Presenter 인터페이스이며 비즈니스 로직에 해당하는 메소드가 특정되어있어 해당 인터페이스를 implements한 클래스에서 반드시 구현해야한다. 뷰와 마찬가지로 비즈니스로직이 추가될때 Presenter 인터페이스를 확인하면 어떠한 기능들이 구현되어있는지 한눈에 볼 수 있다.

MainPresenter

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class MainPresenter implements Presenter{
    private MainView view;
    private NewsData model;
    private ArrayList<NewsData> news =  new ArrayList<>();
    private RequestQueue queue;
    public MainPresenter(MainView view){
        this.view = view;
        this.model = new NewsData();
    }
    public ArrayList<NewsData> getNews() { return news; }
    @Override
    public void addNewsList(String title, String description, String imgUrl) {
        this.model = new NewsData();
        model.setTitle(title);
        model.setDescription(description);
        model.setUrlToImage(imgUrl);
        news.add(model);
    }
    public void getNewsJson(Context context) {
        queue = Volley.newRequestQueue(context);
        StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {
                        try {
                            JSONObject jsonObj = new JSONObject(response);
                            JSONArray arrayArticles = jsonObj.getJSONArray("articles");
                            for(int i = 0, j = arrayArticles.length(); i<j; i++){
                                JSONObject obj = arrayArticles.getJSONObject(i);
                                addNewsList(obj.getString("title"),
                                        obj.getString("description"),
                                        obj.getString("urlToImage"));
                            }
                            view.showNewsList();
                        } catch (JSONException e){
                            e.printStackTrace();
                        }
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) { }
        });
        queue.add(stringRequest);
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 

MainPresenter 생성자와 addNewsList(), getNewsJson() 메소드가 구현되어있다. 

 

 

MainActivity

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
29
30
31
32
33
public class MainActivity extends AppCompatActivity implements MainView{
    private RecyclerView recyclerView;
    private RecyclerView.Adapter mAdapter;
    private RecyclerView.LayoutManager layoutManager;
    MainPresenter presenter = new MainPresenter(this);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_news);
        recyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);
        recyclerView.setHasFixedSize(true);
        layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        presenter.getNewsJson(this);
    }
    @Override
 
    public void showNewsList() {
        mAdapter = new MyAdapter(presenter.getNews(), MainActivity.thisnew View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Object obj = v.getTag();
                if(obj != null){
                    int position = (int)obj;
                    Intent intent = new Intent(MainActivity.this, NewsPageActivity.class);
                    intent.putExtra("news",((MyAdapter)mAdapter).getNews(position));
                    startActivity(intent);
                }
            }
        });
        recyclerView.setAdapter(mAdapter);
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 

MainPresenter를 생성하고 Presenter에 구현되어있는 getNewsJson() 메소드로 뉴스 데이터를 받아와 데이터가 있을 경우 NewsData 모델을 생성하여 가공하고 NewList에 차곡차곡 저장하여두고 작업이 완료되면 View에 해당하는 메인액티비티에 구현된 showNewsList() 메소드를 실행하도록 하였다. 이제 매인 액티비티에는 Presenter에서 가공하여 리스트가 만들어진 것을 그대로 화면에 표현만 해주게 되고 각각의 아이템을 클릭했을때 자세히 보여줄 액티비티로 이동하게될 로직만 추가되어있다.

NewsPageActivity도 동일하게 작성되었으나 아직까지는 인텐트로 받은 key를 통해 해당 key의 NewsData를 찾아서 화면에 표시하는 기능만 있어서 presenter를 추가하지는 않았다. 여기에도 많은 기능들이 추가된다면 NewsPagePresenter를 생성해서 view와 presenter를 구분하여 코드를 작성하면 될 것이다.



실행하면 mvc와 동일하게 동작하는 것을 확인할 수 있다. 
어렵다... 참고 예제와 구글의 MVP 예제를 비교하면서 보고있는데 틀린 부분도 많은 것 같고 아직 이해하지 못한 부분도 많은 것 같다. adapter를 view와 presenter로 구분하는 부분도 하지 못했다. 다만 Presenter 인터페이스와 View 인터페이스가 구분되고 View에 관련된 로직과 비즈니스 로직이 대충 어떠한 방식으로 구분되는것인지 파악이 된 것 같다.  아무래도 구글 mvp를 좀 더 자세하게 구분해보는 것이 좋을 것 같다. 

다음 글에서는 구글의 mvp 예제인 todo-mvp를 참고하여 예제를 작성하여 한번 더 mvp에 대해 자세하게 알아보도록 해야겠다. 



참고
https://thdev.tech/androiddev/2016/10/12/Android-MVP-Intro/
https://academy.realm.io/kr/posts/eric-maxwell-mvc-mvp-and-mvvm-on-android/
https://thdev.tech/androiddev/2016/10/12/Android-MVP-Intro/