안드로이드 Architecture 패턴 Part 2: 모델-뷰-프레젠터(Model-View-Presenter)

Jong Yun Lee
Nspoons
Published in
12 min readMar 9, 2017

출처 : https://upday.github.io/blog/model-view-presenter/

이제 정말 우리 개발자들이 어떻게 좋은 패턴 구조를 안드로이드 앱에 적용할 것인지 생각해야할 때입니다. 이를 위해서, 구글은 Erik Hellman과 제가(Florina Muntenescu) 함께 작업한 MVP 그리고 RxJava 샘플로 안드로이드 구조의 청사진 (Android Architecture Blueprints)을 제공합니다. 저희가 어떻게 적용을 하였고 이 접근법의 장단점이 무엇이 있는지 함께 살펴보도록 합시다.

모델-뷰-프레젠터(Model-View-Presenter) 패턴

여기에 모든 요소들과 그들의 역할을 정리해 보았습니다.

  • Model-데이터 계층입니다. 비지니스 로직을 다루는 것과 네트워크, 데이터베이스 계층과 소통하는 역할을 합니다.
  • View-UI 계층입니다. 데이터를 보여주고 Presenter에게 유저의동작을 알려줍니다.
  • Presenter-Model로부터 데이터를 받아 UI 로직에 적용하며 뷰의 상태를 관리합니다. 어떤것을 보여줄 것인지 결정하고, View로 부터 오는 유저의 인풋에 대해서 반응합니다.

View와 Presenter가 아주 긴밀하게 함께 동작하기 때문에, 둘은 서로의 참조자를 가지고 있을 필요가 있습니다. Presenter를 JUnit을 이용해 유닛 테스트가 가능하도록 하려면, View가 추상화 되고 인터페이스가 됩니다. Presenter와 짝을 이루는 View 사이의 관계는 Contract 라는 클래스에 정의 되게 되고, 그 결과 코드가 훨씬 쉽게 읽을 수 있게 되며 그들사이의 관계 또한 이해하기 쉽게 됩니다.

Model-View-Presenter 클래스 구조

안드로이드 구조 청사진에 담긴 Model-View-Presenter 패턴과 RxJava

이 청사진 셈플은 “To Do” 어플리케이션 입니다. 이 앱은 유저가 할일 목록을 작성하고, 확인하고, 수정하고, 삭제할 수 있도록 해줍니다. 물론 보여지는 할일 목록을 필터링 할 수 있도록도 해주지요. 이 때, Main thread로부터 벗어나 비동기적인 동작들을 할 수 있도록 하기 위해 RxJava가 이용됩니다.

Model

Model 원격(remote)의 혹은 로컬에 있는 데이터를 가지고 동작합니다. 모델은 비지니스 로직이 다뤄지는 곳이죠. 예를들어, 작업(Task) 목록을 요청할 때에 모델은 로컬 데이터 소스로 부터 목록을 받으려고 할 것입니다. 만약 목록이 비었다면, 네트워크에 요청하여 찾아본 후 로컬 데이터 소스에 요청에 대한 응답 내용을 저장할 것입니다.

작업 목록을 가져오는 것은 RxJava의 도움으로 다음과 같이 할 수 있을 것입니다:

public Observable<List<Task>> getTasks(){
...
}

모델은 로컬과 원격의 데이터 소스와의 인터페이스 생성자(constructor)에서 인자(parameters)들로 받게 되고, 모델을 안드로이드이 다른 어떤 클래스와 전적으로 독립적으로 되도록 하며 그 결과 JUnit으로 테스트 하기가 쉬워집니다. 예를 들어 getTasks 가 로컬 데이터 소스로 부터 요청을 잘 보내는지 테스트 하기 위해 다음과 같이 테스트를 구현할 수 있습니다.

@Mock
private TasksDataSource mTasksRemoteDataSource;

@Mock
private TasksDataSource mTasksLocalDataSource;
...

@Test
public void getTasks_requestsAllTasksFromLocalDataSource() {
// Given that the local data source has data available
setTasksAvailable(mTasksLocalDataSource, TASKS);
// And the remote data source does not have any data available
setTasksNotAvailable(mTasksRemoteDataSource);

// When tasks are requested from the tasks repository
TestSubscriber<List<Task>> testSubscriber = new TestSubscriber<>();
mTasksRepository.getTasks().subscribe(testSubscriber);

// Then tasks are loaded from the local data source
verify(mTasksLocalDataSource).getTasks();
testSubscriber.assertValue(TASKS);
}

View

View는 데이터를 보여주기 위해 Presenter와 함께 작동하며, Presenter에게 유저의 동작을 Presenter에게 알려줍니다. MVP Activity에서, Fragment와 커스텀 안드로이드 뷰들은 모두 이 View 라고 할 수 있습니다. 우리의 선택은 Fragment를 사용하는 것이었습니다.

모든 View들은 Presenter를 설정하는 동일한 BaseView 인터페이스를 구현합니다.

public interface BaseView<T> {

void setPresenter(T presenter);

}

View는 onResume 에서 subscribe 함수를 호출함으로써 Presenter에게 업데이트될 준비가 되었다고 알려줍니다. View는onPause() 에서 Presenter에게 더이상 본인이 업데이트 될 필요가 없음을 말해주기 위해 presenter.unsubscribe() 을 호출합니다. 만약 안드로이드 커스텀 뷰에 View를 구현하는 것이라면, subscribeunsubscribe 메소들은각각 onAttachedToWindowonDetachedFromWindow 에서 호출되어야 합니다. 버튼 클릭과 같은 유저의 동작은 Presenter에 있는 상응되는 메소들이 실행되도록 촉발시킬 것이며, 다음에 어떤 일이 일어나야 할지를 결정할 것입니다.

View들은 Espresso를 이용하여 테스트 될 수 있습니다. 예를들어 통계와 관련된 스크린에서는 수시로 변하는 작업(task)들의 숫자를 보여줄 필요가 있습니다. 테스트는 잘 진행이 되었는지 확인하기 위해서 작업들(tasks)을 TaskRepository 에 넣고, StatisticsActivity 를 시작하고, 뷰의 구성을 확인합니다.

@Before
public void setup() {
// Given some tasks
TasksRepository.destroyInstance();
TasksRepository repository = Injection.provideTasksRepository(
InstrumentationRegistry.getContext());
repository.saveTask(new Task("Title1", "", false));
repository.saveTask(new Task("Title2", "", true));

// Lazily start the Activity from the ActivityTestRule
Intent startIntent = new Intent();
mStatisticsActivityTestRule.launchActivity(startIntent);
}

@Test
public void Tasks_ShowsNonEmptyMessage() throws Exception {
// Check that the active and completed tasks text is displayed
Context context = InstrumentationRegistry.getTargetContext();
String expectedActiveTaskText = context
.getString(R.string.statistics_active_tasks);
onView(withText(containsString(expectedActiveTaskText)))
.check(matches(isDisplayed()));
String expectedCompletedTaskText = context
.getString(R.string.statistics_completed_tasks);
onView(withText(containsString(expectedCompletedTaskText)))
.check(matches(isDisplayed()));
}

Presenter

Presenter와 이와 상응되는 View는 Activity에 의해서 생성됩니다. View와 TaskRepository 의 참조자는 Presenter의 생성자에서 부여됩니다. constructor의 구현에서, Presenter는 뷰의setPresenter 를 호출할 것입니다. 이런 구조는 Presenter를 상응되는 뷰에 연결해주는 의존성 주입(dependency injection) 을 이용하면 클래스들이 커플링 되는것을 줄여주고 매우 간단한 코드가 됩니다. Dagger를 이용해서 Todo-MVP를 구현하는 것은 이곳에서 볼 수 있습니다.

모든 Presenter는 동일한 BasePresenter 인터페이스를 구현합니다.

public interface BasePresenter {

void subscribe();

void unsubscribe();

}

subscribe 메소드가 호출 되었을때, Presenter는 Model에게 데이터를 요청하게 됩니다, 그 다음 그 데이터에 UI 로직을 적용하여 View에 넣어주게 되죠. 예를들어 StatisticsPresenter 에서, 모든 할일들은 TaskRepository 로부터 나온 다음, 취해진 할일들은 활성화된 혹은 완료된 할일 개수를 계산하는데 사용됩니다. 이 개수는 뷰의 메소드인
showStatistics(int numberOfActiveTasks, int numberOfCompletedTasks) 의 인자로 사용됩니다.

showStatistics 메소드가 알맞은 값으로 호출되는지 테스트 하기위한 유닛 테스트는 구현하기 쉽습니다. TaskRepositoryStatisticsContract.View를 moking 하고 mock 된 객체들을 StatisticsPresenter 의 생성자의 인자로 줄 것입니다. 테스트가 구현된 모습입니다 :

@Test
public void loadNonEmptyTasksFromRepository_CallViewToDisplay() {
// 생성된 StatisticsPresenter를
// active task 1개, completed task 2개와 함께 준비합니다
setTasksAvailable(TASKS);

// Tasks 로딩이 요청될때
mStatisticsPresenter.subscribe();
// 그 다음 view에 올바른 데이터를 전달합니다
verify(mStatisticsView).showStatistics(1, 2);
}

unsubscribe 의 역할은 메모리 누수를 피하기 위해 Presenter의 모든 subscriptions들을 정리해 없애는 것입니다.

subscribeunsubscribe 와는 별개로, Presenter는 View의 유저 동작과 상응되어 다른 메소드들을 노출시킵니다. 예를들어, AddEditTaskPresentercreateTask 같은 메소드들을 추가합니다. 이 메소드는 유저가 새로운 할일을 만들기 위해 버튼을 눌렀을때 호출 될 것입니다. 이 점으로 인해서 유저의 모든 동작들이 Presenter를 통해 UI 로직을 거치게 되며, 결국 유닛 테스트가 가능하게 됩니다.

Model-View-Presenter 패턴의 단점

Model-View-Presenter 패턴은 주의가 필요한 것들의 분할을 잘 할 수 있도록 해줍니다. 이것이 아주 좋은 장점임과 동시에, 작은 앱이나 프로토타입을 개발할때에는 너무 많은 불필요한 작업처럼 느껴질 수도 있습니다. 사용되는 인터페이스의 개수를 줄이기 위해서, 몇몇 개발자들은 Contract 인터페이스 클래스와 Presenter를 위한 인터페이스를 제거합니다.

결론

Model-View-Controller 패턴은 두가지 주요한 단점들이 있습니다. 첫째로, View가 Controller와 Model 모두에 대한 참조자를 가지게 됩니다. 두번째로, 이것이 UI 로직을 하나의 클래스를 담는데에 한계가 있으며, 그 책임이 Controller와 View 또는 Model 사이에 공유되게 됩니다. Model-View-Presenter 패턴은 이 두가지 문제를 해결하기 위해서 Model과 View 가 가지고 있는 연결을 끊고 View에 대해 present하는 모든 것들을 다루는 단 하나의 클래스를 생성합니다 — 바로 Presenter입니다. 이 하나의 클래스로 인해서 유닛테스트가 쉬워 지게 됩니다.

만약 우리가 이벤트 기반의 구조를 원하다고 한다면, 어디서 View가 변화에대해 반응해야 할까요? Android Architecture Blueprints에 있는 다음 예시 패턴을 유념해 주세요. 그때까지 upday app에 있는 Model-View-ViewModel 패턴을 읽어보시길 바랍니다.

--

--