이 포스트를 보기 전에 "안드로이드 mvp와 mvvm 예제로 알아보자(1) - 기본 구현" 포스트를 대충이라도 훑어봐야 한다. 그렇지 않으면 해당 포스트 이해가 힘들 것이다. 


※ MVP(Model - View - Presenter 란?

: MVP 설계는 Model - View - Presenter을 읽컫는 말로 모델과 뷰, 프레젠터라는 세 가지 역할로 나뉜다. 지금 말하는 거에 대해서 이해가 안 될 수 있다. 앞에서 차차 볼 프로젝트 예제를 본 후에 다시 지금 내용을 보면 이해가 갈 것이다. 모델에는 데이터와 비즈니스 로직이 들어가고 뷰는 데이터를 표시한다. 프레젠터는 뷰와 모델 사이에서 서로를 통신하는 역할을 한다. UI를 보여주는 데 있어서 뷰가 모델에 있는 데이터가 필요하다면 프레젠터를 이용한다. 마찬가지로 모델에 있는 데이터가 갱신되어 UI를 바꿔야 한다면 모델이 직접 뷰에 접근하는 게 아니라 프레젠터에 해당 작업을 위임한다. 많이 추상적이여서 이해가 안 될 것이다. 다음의 예를 보면서 알아보자. 다음 예제는  "안드로이드 mvp와 mvvm 예제로 알아보자(1) - 기본 구현" 포스트 에서 본 예제를 MVP 설계를 바탕으로 구현한 것이다. 예제 소스는  https://github.com/wikibook/advanced-android-book/tree/master/tech05/NewGitHubRepos 이다.




Ex) mvp를 이용한 예제(예제 소스의 app-mvp)



: mvp 에는 contract, model, presenter, view 패키지를 가진다. contract는 뷰와 프레젠터가 구현해야할 인터페이스가 정의되어 있다. 


1. RepositoryListContract 인터페이스

contract 패키지 내의 인터페이스는 뷰와 프레젠터가 구현해야할 인터페이스가 정의된다. 아래 코드를 보면 View가 구현할 View 인터페이스와 Presenter가 구현할 Presenter 인터페이스가 있다. 인터페이스를 정의하는 이유는 프레젠터와 뷰 사이의 통신을 위해서이다. 뷰에서는 UI 이벤트(ex. 클릭 이벤트)가 들어올 때 모델에 있는 데이터와 관련된 작업을 하려면 프레젠터에 접근해야 한다. 프레젠터는 이에 대한 결과를 UI 상에 보여주기 위해서는 뷰에 접근해야 한다. 이렇게 둘 사이에 통신을 하기 위해서 인터페이스를 정의한다. 프레젠터는 액티비티가 아닌 인터페이스로 뷰를 조작할 수 있다. 

/**
 * 각자의 역할이 가진 Contract(계약)를 정의해 둘 인터페이스
 */
public interface RepositoryListContract {

  /**
   * MVP의 View가 구현할 인터페이스
   * Presenter가 View를 조작할 때 이용한다
   */
  interface View {
    String getSelectedLanguage();
    void showProgress();
    void hideProgress();
    void showRepositories(GitHubService.Repositories repositories);
    void showError();
    void startDetailActivity(String fullRepositoryName);
  }

  /**
   * MVP의 Presenter가 구현할 인터페이스
   * View를 클릭했을 때 등 View가 Presenter에 알릴 때 이용한다
   */
  interface UserActions {
    void selectLanguage(String language);
    void selectRepositoryItem(GitHubService.RepositoryItem item);
  }
}


2. RepositoryListActivity

: onCreate()에서 Presenter을 생성한다. 인자로 ~Contract.View로 캐스팅한 this를 넘긴다. Presenter는 View를 조작할 때 인자로 넘어온 this를 이용한다. 이렇게 액티비티가 아닌 인터페이스를 전달하는 이유는 자료형을 교환하기 위해서다.  만약 테스트를 할 시 해당 인터페이스를 테스트용 구현으로 교환이 가능하다. setupViews()는 이전 포스트에서 알아본 setupViews()와 거의 동일하다. 다만 스피너의 항목을 선택했을 때 불려지는 메서드 하나가 다르다. 이전 포스트에서는 loadRepositories(language)가 호출됬었다. 이는 RepositoryListActivity에 구현됬었던 메서드였다. 하지만 MVP 설계에서는 ~Presenter.selectLanguage() 메서드 호출을 통해 프레젠터에게 뷰 이벤트 발생을 알리고 이에 필요한 작업을 프레젠터에게 위임한다. 즉, 이전 포스트에 있던 loadRepositories() 작업을 뷰(Activity)에서 직접 하는 게 아니라 프레젠터가 하게 되고 프레젠터가 직접 모델에 접근해서 RecyclerView의 목록을 가져온다. 또한 loadRepositories 메서드에서는 프로그래스 바 보이기/감추기, 스택바 보이기 같은 UI를 변경하는 메서드들이 있었다. 이런 사소한 UI 변경 작업도 뷰가 하지 않고 프레젠터에 위임한 것이다. 가져온 목록 데이터를 뷰에 보여주는 코드는 아래 코드의 다음 코드에서 볼 것이다. 아래 코드의 마지막을 보면 RecyclerView 목록 리스너 구현 부분이 나온다. 마찬가지로 뷰에서 직접 작업을 하지 않고 프레젠터에 위임한 걸 알 수 있다.

public class RepositoryListActivity extends AppCompatActivity implements RepositoryAdapter.OnRepositoryItemClickListener,
    RepositoryListContract.View {

    ......

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_repository_list);

    // View를 설정
    setupViews();

    // Presenter의 인스턴스를 생성
    final GitHubService gitHubService = ((NewGitHubReposApplication) getApplication()).getGitHubService();
    repositoryListPresenter = new RepositoryListPresenter((RepositoryListContract.View) this, gitHubService);
  }

  /**
   * 목록 등의 화면 요소를 만든다
   */
  private void setupViews() {

    ....View 초기화 및 Adpater 설정

    languageSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
      @Override
      public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        //  스피너의 선택 내용이 바뀌면 호출된다
        String language = (String) languageSpinner.getItemAtPosition(position);
        //  Presenter에 프로그래밍 언어를 선택했다고 알린다
        repositoryListPresenter.selectLanguage(language);
      }

      @Override
      public void onNothingSelected(AdapterView<?> parent) {

      }

    });
  }

  /**
   * RecyclerView에서 클릭됐다
   * @see RepositoryAdapter.OnRepositoryItemClickListener#onRepositoryItemClickListener
   */
  @Override
  public void onRepositoryItemClick(GitHubService.RepositoryItem item) {
    repositoryListPresenter.selectRepositoryItem(item);
  }

  ..... 다음 부분에서 볼 예정

}

인제 ~Contract.View 인터페이스를 구현한 부분을 보자. 해당 메서드들은 프레젠터가 뷰를 조작하기 위해 사용된다. 아래 코드를 보면 대부분이 뷰를 조작하는 메서드인 걸 알 수 있다. 이들 메서드들은 Presenter에서 호출된다.

// =====RepositoryListContract.View 구현===== // 이곳에서 Presenter로부터 지시를 받아 View의 변경 등을 한다 @Override public void startDetailActivity(String full_name) { DetailActivity.start(this, full_name); } @Override public String getSelectedLanguage() { return (String) languageSpinner.getSelectedItem(); } @Override public void showProgress() { progressBar.setVisibility(View.VISIBLE); } @Override public void hideProgress() { progressBar.setVisibility(View.GONE); } @Override public void showRepositories(GitHubService.Repositories repositories) { // 리포지토리 목록을 Adapter에 설정한다 repositoryAdapter.setItemsAndRefresh(repositories.items); } @Override public void showError() { Snackbar.make(coordinatorLayout, "읽을 수 없습니다", Snackbar.LENGTH_LONG) .setAction("Action", null).show(); }


3. RepositoryListPresenter

: 뷰에서 호출된 Presenter의 selectLanguage 메서드를 보자. 우리가 이전 포스트에서 보았던 loadRepositories() 메서드가 호출된다. loadRepositories() 메서드를 보면 이전 포스트의 loadRepositories()와 거의 똑같다. UI의 변경 부분만 다르다. UI 변경은 ~Contract.View 에서 정의된 메서드를 통해 한다. 그래서 RepositoryListActivity에서 정의한 showProgress, hideProgress, showRepositories 등을 통해 UI를 변경한다. 

public class RepositoryListPresenter implements RepositoryListContract.UserActions { ....변수 선언 및 초기화 @Override public void selectLanguage(String language) { loadRepositories(); } @Override public void selectRepositoryItem(GitHubService.RepositoryItem item) { repositoryListView.startDetailActivity(item.full_name); } /** * 지난 일주일간 만들어진 라이브러리의 인기순으로 가져온다 */ private void loadRepositories() { // 로딩 중이므로 진행바를 표시한다 repositoryListView.showProgress(); // 일주일 전 날짜 문자열 지금이 2016-10-27이면 2016-10-20 이라는 문자열을 얻는다 final Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, -7); String text = DateFormat.format("yyyy-MM-dd", calendar).toString(); // Retrofit을 이용해 서버에 액세스한다 // 지난 일주일간 만들어지고 언어가 language인 것을 쿼리로 전달한다 Observable<GitHubService.Repositories> observable = gitHubService.listRepos("language:" + repositoryListView.getSelectedLanguage() + " " + "created:>" + text); // 입출력(IO)용 스레드로 통신해 메인스레드로 결과를 받아오게 한다 observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Subscriber<GitHubService.Repositories>() { @Override public void onNext(GitHubService.Repositories repositories) { // 로딩을 마쳤으므로 진행바 표시를 하지 않는다 repositoryListView.hideProgress(); // 가져온 아이템을 표시하기 위해, RecyclerView에 아이템을 설정하고 갱신한다 repositoryListView.showRepositories(repositories); } @Override public void onError(Throwable e) { // 통신에 실패하면 호출된다 // 여기서는 스낵바를 표시한다(아래에 표시되는 바) repositoryListView.showError(); } @Override public void onCompleted() { // 아무것도 하지 않는다 } }); } }


RepositoryAdapter를 비롯한 나머지 소스 파일들은 이전 포스트와 동일하다. 위 예제에서 보다시피 프레젠터는 뷰의 규현에 대해 자세히 알 필요가 없다. 단지 상위 인터페이스에 정의된 메서드만 호출하면 되기 때문이다. 뷰도 마찬가지다. 이렇게 역할을 명확히 나눌 수 있다.


※ MVP 장점

: 모델, 뷰, 프레젠터의 역할이 명확히 나누어져 코드 관리가 쉽다. 또한 액티비티에 대부분의 구현이 집중되지 않아 작게 만들 수 있다. 프레젠터가 뷰와 모델 사이를 이어주므로 뷰와 모델 서로는 직접적인 의존 관계가 사라진다. 또한 뷰와 프레젠터가 인터페이스를 통해 서로 접근하므로 위에서 말했듯이 교환이 가능해 테스트하기 쉬워진다.


※ MVP 단점

: 모델에서 가져온 데이터를 뷰에 표시하는 것은 개발자가 직접 구현해야해 번거롭다.  MVP 설계에 대한 명확한 기준이 없어서 설계하는 데 난이도가 높다. 뷰와 컨트롤러의 통신을 위해 인터페이스를 정의해야 하는 데 이 인터페이스가 길어지기 쉽다. 그리고 앱이 비대해질수록 프레젠터의 크기도 많이 커지고 복잡해질 가능성이 높다. 위 예제에서 보다시피 Contract 인터페이스 부분에 정의된 메서드를 구현할 때 한 줄로 된 게 많아 복잡하다. MVVM 설계는 MVP의 단점의 일부분을 보완해준다.



이전 포스트와 이번 포스트를 거쳐서 MVP 패턴에 대해서 알아보았다. 다음 포스트에서는 MVP의 단점의 일부분을 보완한 MVVM 설계에 대해서 알아볼 것이다.





+ Recent posts