Android Application Architecture (번역)

기본적인 액티비티와 AsyncTask 에서 시작하여 RxJava 로 만든 MVP 기반 구조까지의 여정.

안드로이드 개발 생태계는 매우 빠르게 변하고 있습니다. 매주 새로운 툴이 만들어지고, 라이브러리들이 업데이트 되며, 블로그 글들이 작성됩니다. 한달동안 휴가를 갔다 돌아오면 새로운 버전의 Support 라이브러리와 Play Service 라이브러리가 나와있을 것입니다.

저는 3년 넘게 ribot 팀과 함께 안드로이드 앱을 만들고 있습니다. 이시간 동안, 우리가 안드로이드 앱을 만들 때 사용하는 구조와 기술들은 계속 진화해왔습니다. 이 글은 우리가 배운점, 실수들, 그리고 구조적 변화가 있었던 이유들에 대한 글입니다.

The old times

2012 년으로 돌아가보면 우리는 기본적인 구조를 따르곤 했습니다. 어떤 네트워크 라이브러리도 사용하지 않았고 AsyncTask 만 우리의 친구였습니다. 아래의 다이어그램이 그 당시의 대략적인 구조입니다.

초기 구조

코드는 두 개의 층으로 구성되어 있습니다. 데이터 층은 REST API 로 부터 데이터를 가져오고 저장하고 데이터 저장소를 영구적으로 보존하는 역할을 합니다. 뷰 층은 UI 에서 데이터를 다루고 표현하는 역할을 가지고있습니다.

APIProvider 는 액티비티들과 프래그먼트들이 쉽게 REST API 와 상호작용할수 있게 만드는 메서드를 제공합니다. 이 메서드는 URLConnection 과 AsyncTask 를 사용해서 각각의 분리된 스레드에서 네트워크 요청을 하고 그 결과를 콜백을 통해서 액티비티들에게 돌려줍니다.

이와 유사하게, CacheProvider 는 SharedPreference 와 SQLite 데이터 베이스 로 부터 데이터를 가져오고 저장하는 메서드를 가지고 있습니다. 이것 역시 액티비티들에게 결과를 돌려주는 콜백을 사용합니다.

The problems

이러한 접근이 가장큰 문제는 뷰 층이 너무 많은 책임을 가지고 있는 것 입니다. 블로그 글 목록을 로딩하는 어플리케이션의 간단한 시나리오를 상상해q봅시다. SQLite 데이터 베이스에서 목록을 캐시하고 마지막으로 ListView 에 목록을 보여줍니다. 액티비티는 다음과 같은 것을 해야합니다.

  1. APIProvider 에서 loadPost(callback) 메서드를 호출합니다.
  2. APIProvider 의 성공 콜백을 기다리고, CacheProvider 에서 savePost(callback) 메서드를 호출합니다.
  3. CacheProvider 의 성공 콜백을 기다리고 글 목록을 ListView 에 보여줍니다.
  4. APIProvider 와 CacheProvider 각각 두개의 에러 콜백을 다룹니다.

이것은 매우 간단한 예제입니다. 일반적인 시나리오에서 REST API 는 아마 뷰가 필요로 하는 데이터 그 자체를 주진 않을것입니다. 그러므로 액티비티는 데이터를 보여주기 전에 약간의 변환과 필터링을 해야합니다. 또 다른 일반적인 경우는 loadPost() 메서드가 다른 곳으로부터 데이터를 가져올 때 파라미터가 필요한 경우입니다. 예를 들면, Play Services SDK 에 의해 제공되어지는 이메일 주소같은 거 입니다. SDK 는 비동기적으로 콜백을 이용해서 이메일을 반환할 것입니다. 즉, 이것은 우리는 세 단계의 중첩된 콜백을 가지는 것을 뜻합니다. 계속해서 복잡성이 더해진다면 콜백 지옥이 발생할 것입니다.

요약:

  • 액티비티와 프래그먼트를 유지보수 하기에 매우 크고 어렵습니다.
  • 매우 많은 중첩 콜백은 코드를 이해하기 어렵게 만들고 변화를 주거나 새로운 기능을 추가할 때 끔찍합니다.
  • 불가능하지는 않지만 유닛 테스트가 힘든 액티비티와 프래그먼트 안에 매우 많은 코드가 있기 때문에 유닛 테스트가 어려워집니다.

A new architecture driven by RxJava

우리는 대략 2년동안 앞에서 설명한 방법을 따라왔습니다. 그 시간동안 위에서 말한 문제들을 해결하기위해 몇가지 개선이 있었습니다. 예를 들면, 액티비티와 프래그먼트에서 코드를 줄이기 위해 몇가지 헬퍼 클래스를 도입했습니다. 그리고 APIProvider 에서 Volley 를 사용하기 시작했습니다. 이러한 변화에도 불구하고 우리의 어플리케이션 코드는 아직 테스트 친화적이지 못했고 콜백 지옥 문제를 여전히 종종 겪었습니다.

2014년이 되서야 RxJava 에 대한 글을 읽어보기 시작했습니다. 이것을 몇가지 간단한 프로젝트들에 사용하기 시작한 이후 마침내 우리는 중첩된 콜백 문제를 해결하는책 될 수 있다는 것을 알았습니다. 당신이 리액티브 프로그래밍에 대해 익숙하지않으면 이 소개를 읽어보길 바랍니다. 간단히 말하자면 RxJava 는 비동기 스트림을 통해서 데이터를 다루게 해주고 데이터를 변환하고 필터링하고 조합하기 위해 스트림에 적용할 수 있는 다양한 연산들을 제공합니다.

이전 몇년동안 우리가 겪은 고통들을 고려하여 새로운 앱의 구조를 생각했습니다. 그리고 아래 그림에서 보는 구조를 생각해냈습니다.

RxJava-driven architecture

이 구조는 첫번째 접근과 유사하게 데이터 층과 뷰 층을 분리했습니다. 데이터 층은 DataManager 와 몇가지 헬퍼들을 가지고 있습니다. 뷰 층은 프래그먼트, 액티비티, 뷰 그룹 처럼 안드로이드 프레임워크 컴포넌트로 이루어져있습니다.

(다이어그램의 세번째 칼럼에 있는) 헬퍼 클래스들은 명확한 책임을 가지고 있고 간단한 방식으로 구현했습니다. 예를 들어 대부분의 프로젝트들은 REST API 에 접근하고, 데이터 베이스로 부터 데이터를 읽고, 써드 파티 SDK 들과 상호작용하기 위한 헬퍼들을 가지고 있습니다. 다양한 어플리케이션들은 다양한 수의 헬퍼들을 가질 것입니다 하지만 대부분 공통적인 것들도 있습니다.

  • PreferencesHelper: SharedPreferences 로 부터 데이터를 읽고 저장합니다.
  • DatabaseHelper: SQLite 데이터 베이스에 접근하는 것을 다룹니다.
  • Retrofit Services: REST API 들을 호출합니다. Retrofit 이 RxJava 를 지원하기 때문에 Volley 대신 사용하기 시작했습니다. 그리고 사용하기도 매우 편합니다.

헬퍼안의 대부분의 Public 메서드들은 RxJava Observable 을 반환합니다.

DataManager 는 구조의 핵심입니다. 이것은 헬퍼 클래스들로 부터 얻은 데이터를 RxJava 연산들을 사용하여 결합하고 필터링하고 변환합니다. DataManger 의 목표는 액티비티와 프래그먼트가 이미 준비되어서 더이상 가공이 필요없는 데이터를 제공하기위해 해야되는 많은 일들을 줄이는 것입니다.

아래 코드를 통해서 DataManager 의 메서드가 하는 일을 살펴보겠습니다. 아래 예제는 다음과 같은 것들을 합니다.

  1. Retrofit 서비스를 호출해서 REST API 로 부터 블로그 포스트의 목록을 가져옵니다.
  2. 캐싱을 위해 DatabaseHelper 를 사용하여 로컬 데이터베이스에 포스트들을 저장합니다.
  3. 뷰 층에서 보여주기 원하는 오늘 쓰여진 블로그 포스트들을 필터링합니다.

액티비티 혹은 프래그먼트와 같은 뷰 층의 컴포넌트들은 이 메서드를 호출하고 반환된 Observable 을 구독합니다. 구독이 되면 Observable 이 제공하는 다른 포스트들은 RecyclerView 나 유사한 뷰에 표시되기 위해 Adapter 에 직접 추가될 수 있습니다.

구조의 마지막 요소는 이벤트 버스입니다. 이벤트 버스는 데이터 층에서 발생하는 이벤트들을 전파합니다. 그리고 뷰 층의 다양한 컴포넌트들이 이 이벤트들을 구독합니다. 예를 들어, DataManager 에서 signOut() 메서드는 Observable 이 완료됬다는 이벤트를 발생시킵니다. 이것을 구독하는 다양한 액티비티들이 로그아웃 상태를 보여주기 위해 UI 를 바꿀 수 있습니다.

Why was this approach better?

  • RxJava Observables 와 연산들은 중첩 콜백을 가질 필요를 없애줍니다.
  • DataManager 이전에 뷰 층이 가지던 책임을 넘겨받았습니다. 그러므로 액티비티와 프래그먼트가 더 가벼워 졌습니다.
  • 액티비티와 프래그먼트에서 DataManager 로 코드가 이동하였고 헬퍼들은 유닛 테스트를 더 쉽게 만들어줍니다.
  • 책임의 분명한 분리와 데이터 층과 교류하는 부분이 오직 DataManager 하나를 가지는 것은 구조를 테스트 친화적으로 만들어줍니다. 헬퍼 클래스들 또는 DataManger 는 쉽게 목으로 만들어 질 수 있습니다.

What problems did we still have?

  • 매우 크고 복잡한 프로젝트에서 DataManger 는 매우 커지고 유지하기 어려워집니다.
  • 액티비티와 프래그먼트와 같은 뷰 층의 컴포넌트들은 더 가벼워지지만, 여전히 상당한 양의 RxJava 구독을 다루고, 에러를 분석하는 등의 코드들이 존재합니다.

Integrating Model View Presenter

예전에 MVP 혹은 MVVM 과 같은 구조적 패턴들은 안드로이드 커뮤니티에서 인기를 얻어왔습니다. 샘플 프로젝트이 글에서 이런한 패턴들을 탐구한 뒤 우리의 기존 접근에 MVP 패턴이 의미있는 개선을 줄 수 있다는 것을 알아냈습니다. 현재 구조는 (뷰와 데이터) 두 가지 층으로 나뉘어져있기 때문에, MVP 를 추가하는 것은 자연스럽습니다. 간단히 프리젠터 층을 추가하고 뷰에서 프리젠터로 코드를 옮겼습니다.

MVP-based architecture

데이터 층은 남겨두고 이제는 패턴의 이름과 일치하게 모델이라고 명명했습니다.

프리젠터들은 모델로 부터 데이터를 가져오고 결과가 준비됬을 때 뷰에서 올바른 메서드를 호출하는 역할을 합니다. 프리젠터들은 DataManager 로 부터 반환된 Observable 들을 구독합니다. 그러므로 프리젠터들은 schedulerssubscriptions 같은 것을 다룹니다. 더 나아가 그것들은 에러코드를 분석하거나 필요하다면 데이터 스트림에 추가적인 연산을 할 수 도 있습니다. 예를 들어, 어떤 데이터가 필터링 될 필요가 있는데 다른데에서 재사용되는 필터가 아니라면, 이러한 것은 DataManager 가 아닌 프리젠터에 구현하는게 더 합리적입니다.

아래서 볼 수 있듯이 프리젠터의 퍼블릭 메서드는 다음과 같습니다. 이 코드는 이전 섹션에서 정의한 dataManager.loadTodayPosts() 메서드가 반환하는 Observable 을 구독합니다.

mMvpView 는 프리젠터가 도와주는 뷰 컴포넌트입니다. 보통 MVP 뷰는 액티비티, 프래그먼트 또는 뷰 그룹의 인스턴스입니다.

이전 구조와 마찬가지로, 뷰 층은 뷰 그룹, 프래그먼트, 액티비티와 같은 기본 프레임워크 컴포넌트를 포합합니다. 주요 차이점은 이 컴포너느들이 직접 Observable 을 구독하지 않는다는 것입니다. 대신 그것들은 MvpView 인터페이스를 구현하고 showError(), showProgressIndicat0r() 와 같이 간결한 메서드 목록을 제공합니다. 또한 뷰 컴포넌트들은 클릭과 같은 유저 상호작용을 다룰 책임이있고 프리젠터에서 올바른 메서드를 호출함으로써 동작합니다. 예를 들어 포스트의 목록을 불러오는 버튼을 가지고 있다면 액티비티는 presenter.loadTodayPosts() 를 onClick 리스너로 부터 호출할 것입니다.

MVP 기반의 구조에 대한 전체 샘플을 보고싶다면, GitHub 에서 Android BoilerPlate 프로젝트에서 확인해볼 수 있습니다. ribot 의 아키텍쳐 가이드라인도 읽어볼 수 있습니다.

Why is this approach better?

  • 액티비티와 프래그먼트가 매우 가벼워집니다. 그것들은 UI 를 초기화 하고 업데이트하고 유저 이벤트를 다루는 책임만 가지게됩니다. 그러므로 유지보수하기 더 쉬워집니다.
  • 뷰 레이어를 mock 함으로써 프리젠터를 위한 유닛테스트를 더 쉽게 작성할 수 있습니다. 기존에는 이 코드들이 프리젠터가 아닌 뷰 레이어의 부분이여서 우리는 유닛테스트를 할수 없었습니다. 전체 구조가 매우 테스트 친화적이 되었습니다.
  • DataManager 가 커진다면 프리젠터로 코드를 옮김으로써 이문제를 해결할 수 있습니다.

What problems do we still have?

  • 하나의 DataManager 를 가지는 것은 코드가 매우 커지고 복잡해질 때 여전히 문제가 됩니다. 우리는 아직 이러한 상황까지 도달하지 않았습니다. 하지만 문제가 될 수 있다는 것을 알고있습니다.

완벽한 구조는 없습니다. 이 구조가 모든 문제를 영원히 해결해주는 유일한 하나의 해결책이라고 생각하는 것은 초보적인 생각입니다. 안드로이드 생태계는 빠른 속도로 진화하고 있습니다. 우리가 더 나은 해결책을 찾아 지속적으로 훌륭한 안드로이드 앱을 만들기 위해서 우리는 탐구하고 읽고 실험하면서 따라잡아야 합니다.

이 글을 재밌고 유용하길 바랍니다. 그렇다면 추천 버튼을 눌러주세요. 또 우리의 최근 접근 방법에 대한 당신의 생각을 알려주는 것도 환영합니다.