Hun's Blog

[Android] 디자인패턴 2 - MVC 패턴 본문

Android

[Android] 디자인패턴 2 - MVC 패턴

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

MVC패턴이란?

 

 

- Model, View, Controller의 약자

- 비즈니스 처리 로직과 UI 요소를 분리시켜 서로 영향없이 개발 

- 웹에서 주로 사용되는 디자인패턴 (안드로이드에서는 조금 다른 형태로 표현됨)

 

*Model : 데이터를 가진다.

*View : 사용자에게 보여 질 화면을 표현한다.

*Control : 사용자로부터 입력을 받고, 이를 모델에 의해 View를 정의한다. 

 

MVC 구조에서 입력은 모두 Control 에서 발생하게 된다. 

이벤트가 발생한 Control에 의해 모듈의 정의와 View의 용도가 결정된다. 

 

 

 

이미지1 웹 MVC 동작 순서

 

 

1. Control : 사용자 이벤트 발생

2. Control : 사용자 이벤트 발생 후 Update가 필요한지 Model에 확인

3. Model : 데이터 Update가 필요한지 여부 판별

4. Model : Update가 필요한 경우 View에 알림

5. View : Model로부터 Update 이벤트 수신

6. View : Model에 필요한 데이터 요청

7. Model : 요청받은 데이터를 View에 전송

8. View : Model로부터 실제 필요한 데이터를 수신받아 View 갱신 

 

 

 

안드로이드의 MVC 

 

*안드로이드에는 Activity 혹은 Fragment와 같은 View들이 View와 Control을 모두 가지고 있다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // View 
        setContentView(R.layout.activity_main);
        RecyclerView recyclerView = findViewById(R.id.rv_main) ;
        recyclerView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Control
            }
        });
    }
}
 
 

 

SetContentView(R.layout.activity_main) -> View 관련된 메소드

recyclerView.setOnClickListener -> Control 관련된 메소드

 

웹에서 적용된 MVC는 View와 Control이 완전히 분리된 상태를 의미한다. 

하지만 안드로이드에는 View와 Control이 함께 공존한다. 

 

동작순서를 보면 아래와 같이 진행 될 것이다.

 

 

이미지2 안드로이드 MVC 동작 순서

 

- 안드로이드에서는 Class 하나로 MVC 가 처리 가능한 구조로 만들어진다. 

- 프로젝트가 커지고 메소드 숫자가 많아지면 코드 파악이 어려워진다. 

- 메소드와 클래스를 적절히 분리하면서 복잡도를 관리해야 한다. 

 

장점

- 개발기간 감소 

*Activity 에서 모든 동작을 처리해주면 된다. 

- 코드 분석 용이

* 오히려 한 곳에 모여있기 때문에 가독성에 유리하다. 

 

단점

- 하나의 클래스에 코드양 증가 

* 액티비티 하나에서 모든 기능을 구현할 수 있기 때문에 코드양이 많아진다.

- 유지보수의 어려움 

- View와 Model의 결합도 상승

*대부분의 코드를 View에서 Model을 직접 호출하여 사용하기 때문에 결합도가 높아진다.

- 테스트코드 작성의 어려움

*작성한다 하더라도 UI 위주의 테스트 코드만 작성 가능하다. 

UI 변경이 아닌 모델을 변경하게 될 경우 복잡해진다.

 

 

 


MVC패턴 예제

뉴스앱 예제https://www.youtube.com/watch?v=cOLm7D-2tfE 

앱은 해당 링크의 예제를 구현하였다.  New api를 이용해 뉴스정보를 json으로 전달받아 액티비티 리사이클러뷰 리스트에 순서대로 뿌려준다. 기사를 클릭하면 해당 기사에 대해 자세히 확인할 수 있는 액티비티로 넘어가 자세한 내용을 읽을 수 있다. 


클래스


디자인 패턴에 관련된 글이기 때문에 자세한 내용은 생략하고 해당 예제를 통해 MVC 패턴에 대해 이해해보도록 하겠다.

일단은 Model, View/Control을 구분지어 본다면,

Model -> NewsData
View/Control -> MainActivity, NewsPageActivity
Adapter -> MyAdapter

정도로 구분할 수 있다.  MyAdapter는 이전 글에서 살펴보았던 Strucatural patterns에 속하는 Adapter 패턴이다. mvc와 adapter 패턴이 동시에 등장한다는 것인데 어떻게 구성되어 있는지 자세히 살펴보자.


NewsData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class NewsData implements Serializable {
    private  String title;
    private  String urlToImage;
    private  String description;
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public String getUrlToImage() {
        return urlToImage;
    }
    public void setUrlToImage(String urlToImage) {
        this.urlToImage = urlToImage;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
}
 
 

뉴스 제목과 뉴스 내용 그리고 뉴스 이미지에 대한 String 값을 가지고 있으며 getter setter를 통해 각각의 값들을 셋팅하거나 호출할 수 있도록 한다. 프로젝트를 만들다보면 자주 등장하는 클래스의 형태이다.


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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class MainActivity extends AppCompatActivity {
    private RecyclerView recyclerView;
    private RecyclerView.Adapter mAdapter;
    private RecyclerView.LayoutManager layoutManager;
    RequestQueue queue;
    @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);
        queue = Volley.newRequestQueue(this);
        getNews();
    }
    public void getNews(){
        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");
                            List<NewsData> news = new ArrayList<NewsData>();
                            for(int i = 0, j = arrayArticles.length(); i<j; i++){
                               JSONObject obj = arrayArticles.getJSONObject(i);
                                NewsData newsData = new NewsData();
                                newsData.setTitle(obj.getString("title"));
                                newsData.setDescription(obj.getString("description"));
                                newsData.setUrlToImage(obj.getString("urlToImage"));
                                news.add(newsData);
                            }
                            mAdapter = new MyAdapter(news, 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);
                        } 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
 

onCreate 내부에 View에 속한 내용들이 작성되어있고 getNews() 메소드가 작성되어있다.
getNews()메소드 내부를 보면 실질적으로 외부 사이트의 api를 활용해 데이터를 받아 json 으로 파싱하고 NewsData Model을 활용하여 제목,내용, 이미지를 셋팅한 후 리사이클러뷰 리스트에 뿌려주는 내용이 작성되어있다. View와 Control이 공존하는 MVC 패턴의 모습을 하고있다는 것을 확인할 수 있다.


MyAdapter

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
47
48
49
50
51
52
53
54
55
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
    private List<NewsData> mDataset;
    private static View.OnClickListener onClickListener;
    public static class MyViewHolder extends RecyclerView.ViewHolder {
        public TextView TextView_title;
        public TextView TextView_description;
        public ImageView ImageView_title;
        public View rootView;
        public MyViewHolder(View v) {
            super(v);
            TextView_title = v.findViewById(R.id.TextView_title);
            TextView_description = v.findViewById(R.id.TextView_description);
            ImageView_title = (SimpleDraweeView) v.findViewById(R.id.ImageView_title);
            rootView = v;
            v.setClickable(true);
            v.setEnabled(true);
            v.setOnClickListener(onClickListener);
        }
    }
    public MyAdapter(List<NewsData> myDataset, Context context, View.OnClickListener onClick) {
        mDataset = myDataset;
        onClickListener = onClick;
        Fresco.initialize(context);
    }
    @Override
    public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent,
                                                     int viewType) {
        LinearLayout v = (LinearLayout) LayoutInflater.from(parent.getContext())
                .inflate(R.layout.row_news, parent, false);
 
        MyViewHolder vh = new MyViewHolder(v);
        return vh;
    }
    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        NewsData news = mDataset.get(position);
        holder.TextView_title.setText(news.getTitle());
        String des = news.getDescription();
        if(!des.equals("null")){
            holder.TextView_description.setText(des);
        }else{
            holder.TextView_description.setText("-");
        }
        Uri uri = Uri.parse(news.getUrlToImage());
        holder.ImageView_title.setImageURI(uri);
    }
    @Override
    public int getItemCount() {
        return mDataset == null ? 0 : mDataset.size();
    }
    public NewsData getNews(int position) {
         return mDataset != null ? mDataset.get(position) : null;
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 

위의 Adapter는 메인 액티비티에 있는 리사이클러뷰에 추가될 item 뷰를 셋팅하기도 하고 NewsData에 접근해 값을 셋팅하기도 한다. Adapter의 특징을 다시 떠올려보면 인터페이스가 불일치해 상호 접근이 불가한 객체들 사이에서 연결해주는 역할을 한다는 것인데 이것을 정리해보자면,

- 리사이클러뷰에 추가될 item 뷰를 셋팅하는 View이다.
- item 뷰에 변경될 값을 Model(NewsData)과 송수신하여 처리하는 Control이다.
- 해당 아이템의 View/Control을 리사이클러뷰가 셋팅되어있는 View/Contorl인 액티비티와 상호 접근이 가능하도록 하는 Adapter이다.

즉, MyAdapter는 View/Control 이면서 Adapter라는 결과가 나온다.
결국 MVC 범주안에 속한다고 볼 수 있으며 코드의 양이 많아질 수 있다는 뜻이 된다.

NewsPageActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class NewsPageActivity extends AppCompatActivity {
    public TextView TextView_title;
    public TextView TextView_description;
    public ImageView ImageView_title;
    public NewsData newsData;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_news_page);
        TextView_title = findViewById(R.id.TextView_title);
        TextView_description = findViewById(R.id.TextView_description);
        ImageView_title = findViewById(R.id.ImageView_title);
        Intent intent = getIntent();
        Bundle bundle = intent.getExtras();
        newsData = (NewsData) bundle.getSerializable("news");
        TextView_title.setText(newsData.getTitle());
        TextView_description.setText(newsData.getDescription());
        Uri uri = Uri.parse(newsData.getUrlToImage());
        ImageView_title.setImageURI(uri);
    }
}
 
 

뉴스리스트에서 뉴스를 클릭했을때 새로운 액티비티를 띄워 해당 뉴스의 정보를 뿌려주는 역할을 하고있다. 즉, 새로운 View/Control이다.



정리 - 

그동안 안드로이드에서 프로젝트를 만들면서 사용하던 방법이 mvc이며 안드로이드는 웹과는 조금 다르게 view와 control이 동시에 있는 것이였구나로 정리가 된다. 

 

안드로이드는 view와 control이 공존하는 mvc 패턴을 기본적으로 사용하며 개발한다는 것이다. 

물론 mvc를 사용하지 않고 액티비티 내부에 데이터에 관련된 내용들을 전부 기입해도 된다. 그런 면에서 기본적으로 사용한다는 말이 안맞을수도 있겠지만 그동안 만들어온 예제와 프로젝트를 봤을때 model에 해당하는 내용들은 분리되어 있고 필요할 때 액티비티에서 호출하여 사용했다는 점에서 mvc패턴을 활용해왔던 것이구나 라고 판단하였다. 

그동안 작성했던 예제들과 혼자 개발하면서 만든 작은 크기의 앱들의 코드 길이가 1000줄 이상 넘어가는 부분은 mvc 패턴에서 나올 수 밖에 없는 부분이구나 라는 생각도 들었다. 물론 클래스 분류를 잘 못한 잘못도 있을 것이다.

 

다음 글에는 뉴스앱을 mvp 패턴으로 변경해 보도록 하겠다. 

 

 

참고 

https://thdev.tech/androiddev/2016/10/23/Android-MVC-Architecture/

https://ko.wikipedia.org/wiki/%EB%AA%A8%EB%8D%B8-%EB%B7%B0-%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC