안드로이드 어플리케이션 아키텍처

2017-04-13
 

안드로이드 개발 생태계는 매우 빠르게 움직입니다. 매주 새로운 툴이 만들어지며 라이브러리가 업데이트되며 블로그의 게시물이 올라오며 커뮤니티에는 많은 문제들로 활발히 논의중입니다. 한 달간 휴가를 다녀온다면 새 버전의 서포트 라이브러리가 당신을 반길 것입니다.

나는 3년 넘게 ribot팀에서 안드로이드 앱 개발을 해왔습니다. 이 기간 동안 사용된 아키텍처와 기술을 지속적으로 발전시켜 왔습니다. 이 글에서는 이러한 아키텍처를 적용하면서 얻은 노하우와 학습방법을 설명할 계획입니다.


이전

2012년 코드 베이스는 기본 구조를 따랐습니다. 네트워킹 라이브러리를 사용하지 않고 AsyncTask를 사용하여 직접 구현하였습니다. 아래 다이어그램은 아키텍처의 대략적인 모습을 보여줍니다.

이 코드는 두 개의 레이어로 구성되어 있습니다.

  • Data Layer: REST API 및 데이터 저장소에서 데이터를 검색/저장하는 역할을 담당.
  • View Layer: UI에서 데이터를 처리하고 표시.

API Provider는 Activity 및 Fragment를 REST API와 쉽게 상호 작용할 수 있게 하는 메서드를 제공합니다. 이러한 메서드는 URLConnection 및 AsyncTask를 사용하여 별도의 스레드에서 네트워크 호출을 수행하고 콜백을 통해 결과를 변환합니다.

비슷한 방식으로 CacheProvider는 SharedPreferences 또는 SQLite에서 데이터를 검색하고 저장하는 역할을 합니다. 또한 콜백을 사용하여 결과를 Activity로 다시 전달합니다.


문제점

이 접근 방식의 큰 문제점은 View Layer가 너무 많은 책임을 가지고 있는 것입니다. 앱이 블로그 게시물의 목록을 로드하고 SQLite 데이터베이스에 저장하고 불러오게끔 캐싱 한 다음 ListView에 표시하는 간단한 일반적인 시나리오를 생각해보십시오.

Activity는 다음과 같은 작업을 합니다.

  1. APIProvider에서 loadPosts(callback) 메서드를 호출합니다.
  2. APIProvider 성공 콜백을 기다린 다음 CacheProvider에서 savePosts(callback)를 호출합니다.
  3. CacheProvider 성공 콜백을 기다렸다가 ListView에 게시물을 표시합니다.
  4. APIProviderCacheProvider에서 발생할 수 있는 두 가지 에러 콜백을 별도로 처리합니다.


이것은 아주 간단한 예입니다. 실제 시나리오에서는 REST API가 뷰에서 필요로 하는 데이터만 주지 않습니다. 따라서 Activity는 데이터를 표시하기 전에 필요로 하는 정보로 재 가공해야 합니다. 또 다른 일반적인 경우는 loadPosts() 메서드가 PlayServices SDK에서 제공하는 이메일 주소와 같이 다른 곳에서 가져와야 하는 매개 변수를 사용하는 경우입니다. SDK가 콜백을 사용하여 이메일을 비동기적으로 반환할 가능성이 있습니다. 이런 시나리오인 경우 3번의 중첩된 콜백이 됩니다. 복잡성을 계속 더해가면 이러한 접근방식은 콜백 지옥이 됩니다.


요약하면:

  • ActivityFragment의 코드가 많아질수록 유지보수가 어렵습니다.
  • 중첩된 콜백이 많으면 코드를 변경하거나 새로운 기능을 추가하기 어렵고 이해하기가 어렵습니다.
  • ActivityFragment에 많은 로직들이 구현되어 있어 유닛 테스팅은 가능하지만 어렵습니다.


RxJava가 주도하는 새로운 아키텍처

우리는 약 2년 동안 이전의 접근 방식을 사용해왔습니다. 그동안 위에서 설명한 문제를 약간 완화한 몇 가지 개선 사항을 만들었습니다. 예를 들어 Activity와 Fragment의 코드를 줄이기 위해 몇 가지 Helper 클래스를 추가했으며 APIProvider에서 Volley를 사용하기 시작했습니다. 이러한 변화에도 불구하고 코드는 아직 테스트 친화적이지 못했으며 콜백 지옥 문제는 여전히 자주 발생했습니다.

2014년 RxJava에 대한 기사를 읽기 시작했습니다. 몇 가지 샘플 앱을 만들어 보면서 이것이 중첩된 콜백 문제에 대한 해결책이 될 수 있다는 생각을 했습니다. 반응형 프로그래밍에 익숙하지 않다면 이 소개글을 읽어 보세요. 즉, RxJava를 사용하면 비동기 스트림을 통해 데이터를 관리할 수 있으며, 데이터를 변환 및 필터링 또는 결합을 위해 많은 Operations를 사용할 수 있습니다.


지난 몇 년간 우리가 겪었던 고통을 고려해 볼 때 새로운 앱의 아키텍처가 어떻게 보이는지 그려지기 시작했습니다. 그래서 우리는 이것을 도입하였습니다.

첫 번째 접근 방식과 마찬가지로 이 아키텍처는 Data와 View Layer로 분리될 수 있습니다. Data Layer는 DataManager와 Helper를 포함합니다. View Layer는 Activity, Fragment, ViewGroup 등과 같은 Android 프레임 워크의 구성 요소로 구성됩니다.

Helper 클래스(다이어그램의 3번째 열)는 매우 구체적인 책임을 지며 간결한 방식으로 구현합니다. 예를 들어 대부분의 프로젝트에는 REST API 액세스, 데이터베이스에서 데이터 읽기 또는 SDK와의 상호 작용 위한 Helper가 있습니다.


가장 일반적인 것은 다음과 같습니다.

  • PreferencesHelper: SharedPreferences에서 데이터를 읽고 저장합니다.
  • atabaseHelper: SQLite 데이터베이스 액세스를 처리합니다.
  • Retrofit Service: REST API에 대한 호출을 수행합니다. RxJava를 지원하기 때문에 Volley 대신 Retrofit을 사용하기 시작했습니다. 또한 사용하는 것이 더 좋습니다.

Helper 클래스 안에 있는 public 메서드의 대부분은 Observable을 리턴합니다. DataManager는 아키텍처의 뇌에 해당합니다. RxJava 연산자를 광범위하게 사용하여 Helper클래스에서 검색 한 데이터를 결합, 필터링 및 변환합니다. DataManager의 목적은 변환이 필요하지 않은 데이터를 제공하여 Activity나 Fragment에서 작업량을 줄이는 것입니다.


아래 코드는 DataManager 메서드가 어떻게 보이는지 보여줍니다. 아래 예제 메서드는 다음과 같이 작동합니다.

  1. Retrofit Service를 호출하여 REST API에서 블로그 게시물 목록을 로드합니다.
  2. DatabaseHelper를 사용하여 캐싱을 위해 로컬 데이터베이스에 게시물을 저장합니다.
  3. View Layer에서 표시하고자 하는 블로그 게시물만 필터링하므로 오늘 작성된 블로그 게시물만 표시됩니다.
public Observable<Post> loadTodayPosts() {
        return mRetrofitService.loadPosts()
                .concatMap(new Func1<List<Post>, Observable<Post>>() {
                    @Override
                    public Observable<Post> call(List<Post> apiPosts) {
                        return mDatabaseHelper.savePosts(apiPosts);
                    }
                })
                .filter(new Func1<Post, Boolean>() {
                    @Override
                    public Boolean call(Post post) {
                        return isToday(post.date);
                    }
                });
}

Activity 또는 Fagrment과 같은 View Layer의 구성 요소는 이 메서드를 호출하고 리턴된 Observable를 통해 RecyclerView에 직접 표시할 수 있습니다. 이 아키텍처의 마지막 요소는 이벤트 버스입니다. 이벤트 버스를 사용하면 Data Layer에서 발생하는 이벤트를 브로드캐스트 할 수 있으므로 View Layer의 여러 구성 요소가 이러한 이벤트를 캐치(Subscriptions) 할 수 있습니다. 예를 들어, Observable이 완료되면 DataManager의 signOut() 메서드는 브로드캐스트 이벤트 보내고 이 브로드캐스트를 수신하고 있는 Activity는 UI를 변경하여 로그아웃 상태를 표시할 수 있습니다.


이 방법이 왜 더 좋습니까?
RxJava Observable과 Operators는 중첩된 콜백이 필요 없습니다.

  • DataManager는 이전에 View Layer의 일부였던 책임을 담당합니다. 따라서 Activity와 Fagrment를 더욱 가볍게 만듭니다.
  • Activity, Fagrment에서 DataManager, Helper로 코드를 이동하면 유닛 테스트 작성이 쉬워집니다.
  • DataManager는 Data Layer의 유일한 상호 작용점이기 때문에 테스트 친화적입니다. Helper 클래스 또는 DataManager는 쉽게 변경 가능합니다.


여전히 문제점이 있습니까?

  • 크고 복잡한 프로젝트의 경우 DataManager가 너무 커져 유지 관리가 어려울 수 있습니다.
  • Activity 또는 Fagrment와 같은 View Layer의 구성 요소가 더 가벼워졌지만 RxJava Subscriptions 관리, 에러 탐지 등과 관련하여 상당한 양의 작업을 처리해야 합니다.


Model View Presenter 통합

작년(2014년)에 MVP 또는 MVVM과 같은 몇 가지 아키텍처 패턴이 Android 커뮤니티에서 인기를 얻었습니다. 샘플 프로젝트 및 기사에서 이러한 패턴을 조사한 결과, MVP가 기존 접근법에 매우 중요한 개선을 가져올 수 있다는 것을 발견했습니다. 현재 아키텍처가 두 개의 레이어(View Layer, Data Layer)로 나누어졌기 때문에 MVP를 추가하는 것이 자연스러웠습니다. 새로운 Presenter Layer를 추가하고 코드의 일부를 View에서 Presenter로 옮겨야 했습니다.

Data Layer는 그대로 유지되지만 패턴 이름과의 일관성을 유지하기 위해 이제는 Model이라고 부릅니다.

Presenter는 Model에서 데이터를 로드하고 결과가 준비되면 View의 설정된 메서드를 호출하는 역할을 담당합니다. 데이터 관리자가 리턴한 Observables에 subscribe 합니다. 따라서 schedulerssubscriptions과 같은 것들을 처리해야 합니다. 또한 필요에 따라 에러 코드 탐지를 하거나 데이터를 재가공하는 등 추가 작업을 할 수 있습니다. 예를 들어 일부 데이터를 필터링해야 하고 이 필터를 다른 곳에서 재사용할 가능성이 없는 경우 데이터 관리자가 아닌 Presenter에서 구현하는 것이 더 적합할 수 있습니다.

아래에서 Presenter의 public 메서드는 어떤 것이 있는지 확인할 수 있습니다. 이 코드는 이전 섹션에서 정의한 dataManager.loadTodayPosts() 메서드에서 반환 한 Observable을 subscribe 합니다.

public void loadTodayPosts() {
    mMvpView.showProgressIndicator(true);
    mSubscription = mDataManager.loadTodayPosts().toList()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.io())
            .subscribe(new Subscriber<List<Post>>() {
                @Override
                public void onCompleted() {
                    mMvpView.showProgressIndicator(false);
                }

                @Override
                public void onError(Throwable e) {
                    mMvpView.showProgressIndicator(false);
                    mMvpView.showError();
                }

                @Override
                public void onNext(List<Post> postsList) {
                    mMvpView.showPosts(postsList);
                }
            });
    }

mMvpView는 이 Presenter가 지원하는 View 구성 요소입니다. 일반적으로 MVP View는 Activity, Fragment 또는 ViewGroup의 인스턴스입니다. 이전 아키텍처와 마찬가지로 View Layer에는 ViewGroup, Fragment 또는 Activity와 같은 안드로이드 프레임 워크 구성 요소가 포함되어 있습니다. 주요 차이점은 이러한 구성 요소가 Observables에 직접 subscribe하지 않는다는 것입니다. 대신 MvpView 인터페이스를 구현하고 showError() 또는 showProgressIndicator()와 같은 간결한 메서드 목록을 제공합니다. 또한 View 구성 요소는 클릭 이벤트와 같은 사용자 상호 작용을 처리하고 그에 따라 설정된 메서드를 Presenter에서 호출하여 작동합니다. 예를 들어 게시물 목록을 로드하는 버튼이 있는 경우 Activity는 onClickListener에서 presenter.loadTodayPosts()를 호출합니다.

이 MVP기반 아키텍처의 전체 예제를 보려면 GitHub의 Android Boilerplate 프로젝트를 확인하거나 ribot의 아키텍처 가이드라인에서 자세한 내용을 읽을 수 있습니다.


이 접근법이 더 좋은 이유는 무엇인가요?

Activity와 Fragment는 매우 가볍습니다. 이것의 유일한 책임은 UI를 설정하거나 업데이트를 하고 사용자 이벤트를 처리합니다. 따라서 유지 관리가 더 쉬워집니다. 이제 View Layer를 변경해도 Presenter를 위한 단위 테스트를 쉽게 작성할 수 있습니다. 이전 코드는 View Layer의 일부가 포함되어 단위 테스트가 쉽지 않았습니다. 전체 아키텍처는 매우 테스트 친화적입니다.
DataManager가 점점 커질 경우 일부 코드를 Presenter로 이동하여 이 문제를 완화할 수 있습니다.


여전히 문제점이 있습니까?

코드가 매우 크고 복잡해지면 단일 데이터 관리자를 갖는 것이 여전히 문제가 될 수 있습니다. 이것이 실제로 문제가 되는 지점에 도달하지 못했지만, 일어날 수 있다는 것을 알고 있습니다.


이것은 완벽한 아키텍처가 아니라는 점은 명백한 사실입니다. 모든 문제를 영원히 해결할 수 있는 완벽한 것이 있다고 생각하지 않는 것이 속 편할 것입니다. Android 생태계는 빠른 속도로 발전해 나갈 것이며 우수한 Android 앱을 계속 개발할 수 있는 더 나은 방법을 찾을 수 있도록 계속 실험해야 합니다. 이 글을 즐겁게 읽었으면 좋겠습니다.