Android Developer Document에서 권장하는 가이드 내용입니다.

https://developer.android.com/jetpack/docs/guide

Android 앱에는 여러가지 구성요소가 포함되며, 이런 내용들은 Manifest 에서 선언되어 있습니다.

ex) Activity, Fragment, Service, Content Provider, Broadcast Receiver

 

사용자는 짧은 시간 내에 여러 앱과 상호작용 할 경우도 많습니다.

앱은 사용자 중심의 다양한 워크플로 및 작업에 맞게 조정될 수 있어야 합니다.

 

예를 들어 SNS 앱에서 사진 앱을 통해 사진을 공유하거나 앱 사용 중 전화나 알림으로 사용 환경이 중단될 수도 있죠.

또한 모바일 기기는 리소스가 제한되어 언제든 일부 앱 프로세스가 종료할 수 있습니다.

 

이러한 환경을 고려할 때, 앱 구성 요소는 개별적이고 비순차적 실행이 될 수 있으며 언제든 구성 요소가 제거 될 수 있습니다.

이러한 이벤트는 직접 제어가 불가하기 때문에 앱 구성 요소에는 앱 데이터나 상태를 저장하면 안됩니다.

그리고 앱 구성 요소가 서로 종속되면 안됩니다.

일반적인 아키텍처 원칙

앱 데이터와 상태를 앱 구성 요소에 저장할 수 없다면?

관심사 분리

Activity나 Fragment에 모든 코드를 작성하는 실수는 흔합니다.

UI 기반 클래스는 UI 및 OS 상호작용 등의 로직만 포함해야 합니다.

이러한 클래스를 최대한 가볍게 유지하면 생명 주기(LifeCycle) 관련 문제들을 피할 수 있습니다.

 

Activity 및 Fragment 구현은 소유의 대상이 아니며 Android OS 와 앱 사이의 계약을 나타내는 클래스일 뿐입니다.

언제든지 OS가 클래스를 제거할 수 있으니 이러한 클래스에 대한 의존성을 최소화 하는 것이 좋습니다.

Model에서 UI 구동

또 하나의 중요한 원칙은 Model에서 UI를 구동해야 한다는 것입니다.

가급적 지속성 있는 Model을 권장합니다.

 

Model은 앱의 데이터 처리를 담당하며, 앱의 View 객체 및 앱 구성요소로부터 독립되어 있습니다.

그래서 앱의 생명 주기 관련 문제의 영향을 받지 않습니다.

지속성 있는 Model이 이상적인 이유입니다.

  • Android OS에서 리소스를 확보하기 위해 앱을 제거해도 사용자 데이터는 삭제되지 않음.
  • 네트워크 연결이 취약하거나 연결되어 있지 않아도 앱이 계속 작동함.

데이터 관리 책임이 잘 정의된 Model 클래스를 기반으로 앱을 만들면 쉽게 테스트하고 일관성을 유지할 수 있습니다.

권장하는 앱 아키텍처

이 섹션에서는 Android Architecture Component 를 사용하여 앱을 구성하는 방법을 보여줍니다.

모든 시나리오에 최적화된 하나의 앱 작성 방법은 없지만 이 아키텍처가 유용한 출발점이 될 것입니다.

일반적인 아키텍처 원칙을 따르는 Android 앱 작성법을 사용하고 있으면 변경할 필요가 없습니다.

사용자 프로필 UI를 가정해보면, 비공개 백엔드 및 REST API를 사용하여 지정된 프로필 데이터를 가져옵니다.

개요

앱을 설계한 후 모듈들이 서로 어떻게 상호작용해야 하는지 다이어그램이 보여줍니다.

각 구성요소가 한 수준 아래의 구성요소에만 종속됨을 볼 수 있습니다.

Activity 및 Fragment는 ViewModel에만 종속됩니다.

 

Repository는 여러 개의 다른 클래스에 종속되는 유일한 클래스입니다.

Repository는 지속성 있는 데이터 모델과 원격 백엔드 데이터 소스에 종속됩니다.

이 설계는 일관되고 쾌적한 사용자 환경을 제공합니다.

 

사용자가 앱을 닫은 후 재사용시 앱이 로컬에 보존하는 사용자 정보가 바로 표시됩니다.

이 데이터가 오래된 경우 앱의 Repository 모듈이 백그라운드에서 데이터 업데이트를 시작합니다.

UI 빌드

UI 에 필요한 데이터는 ViewModel 아키텍처 구성요소에 기반한 ViewModel에서 정보를 유지합니다.

ViewModel 객체는 UI 구성요소에 관한 데이터를 제공하고 모델과 커뮤니케이션하기 위한 데이터 처리 비즈니스 로직을 포함합니다. ViewModel 은 UI 구성요소에 관해 알지 못하므로 구성 변경(세로 -> 가로모드)의 영향을 받지 않습니다.

ViewModel에서는 데이터 객체에 대한 변경 등을 View에 알려야 합니다.

여기에서 LiveData 가 사용됩니다.

 

LiveData는 객체 변경사항을 모니터링 할 수 있으며, Activity/Fragment/Service 와 같은 앱 구성요소의 생명 주기 상태를 고려한 로직을 포함합니다. (LiveData 필드는 생명 주기로 인해 더 이상 필요하지 않은 참조를 자동으로 정리합니다.)

RxJava와 같은 라이브러리를 사용 중이라면 그대로 사용해도 됩니다. 단 앱 생명 주기를 올바르게 처리해야 합니다.

Activity나 Fragment 에서 LiveData 객체를 observe 후 변경시에 대한 콜백 onChanged() 를 사용해 UI를 새로고침할 수 있습니다.

 

LiveData는 생명 주기를 인식하기 떄문에 Activity 나 Fragment의 onStop(), onDestroy() 메서드 등에 대한 처리하지 않아도 됩니다.

Data 가져오기

데이터를 가져오는 방식은 내부 데이터나 외부 API를 통한 데이터를 가져오는 방식들이 있습니다.

예제에서는 Retrofit 라이브러리를 통해 백엔드 REST API를 사용합니다.

 

ViewModel에서 직접 해당 라이브러리를 이용하여 데이터를 LiveData 객체에 할당하는 방법이 있습니다.

이 경우 ViewModel 클래스에 너무 많은 책임이 부여되어 관심사 분리 원칙을 위반하게 됩니다.

또한 ViewModel의 범위는 Activity나 Fragment 생명 주기에 연결되어 있으므로 데이터가 손실될 수도 있습니다.

그래서 Repository 모듈을 사용하여 데이터 작업을 처리합니다.

 

Repository는 지속적인 모델, 웹 서비스, 캐시 등 다양한 데이터 소스 간 중재자로 볼 수 있습니다.

이제 ViewModel이 데이터를 가져오는 방법을 직접 알지 못하기 때문에 Repository에서 서로 다른 데이터 가져오기 구현을 통한 데이터를 ViewModel에 제공할 수 있습니다.

구성요소 간 종속성 관리

Repository 클래스에서 WebService를 통해 데이터를 가져올 경우 WebService 클래스의 종속성을 알아야 합니다.

이러한 상황에서 다른 클래스에서도 WebService가 필요한 경우 코드를 복제해야 합니다.

이런 문제는 아래 설계 패턴을 사용하여 해결할 수 있습니다.

  • 종속성 주입(DI) : 클래스가 자신의 종속성을 구성할 필요 없이 종속성을 정의할 수 있습니다.

     

    런타임시 다른 클래스가 이 종속성을 제공해야 합니다.
    Android 앱에서는 Dagger 2 라이브러리 사용이 좋습니다.
    Dagger 2 는 종속성 트리를 따라 객체를 자동으로 구성하고 종속성을 컴파일 시간에 보장합니다.
  • 서비스 로케이터 : 클래스가 자신의 종속성을 구성하는 대신 종속성을 가져올 수 있는 레지스트리를 제공합니다.

DI 사용보다 서비스 레지스트리 구현이 쉬우므로 DI가 익숙치 않다면 서비스 로케이터 패턴을 사용해도 됩니다.

이 패턴은 코드를 복제하거나 복잡성을 추가하지 않아도 종속성을 관리하기 위한 명확한 패턴을 제공하므로 코드를 확장할 수 있습니다.

이러한 패턴을 사용하면 테스트 및 프로덕션 데이터 가져오기 구현 등을 신속하게 전환할 수 있습니다.

ViewModel 과 Respository 연결

ViewModel에서 DI (Dagger 2) 를 사용하여 Repository를 주입시켜 봅니다.

Repository가 WebService 객체 호출을 하지만 하나의 데이터 소스에만 의존하면 유연성이 떨어집니다.

따라서 매번 WebService를 호출하는 방식은 네트워크 낭비 및 새 쿼리 완료시까지 사용자가 기다려야 하는 문제가 있습니다.

이런한 문제를 해결하기 위해 Repository에 데이터를 캐시하는 소스를 추가해 봅니다.

지속되는 데이터

위에까지 구현 방식은 앱이 나갔다 들어오거나 하는 경우 Repository가 메모리 내 캐시에서 데이터를 가져옵니다.

하지만 장시간 프로세스를 종료 후 들어온다면 다시 네트워크에서 데이터를 가져와야 합니다.

이런 경우 Room 같은 라이브러리가 필요합니다.

이미 SQLite 같은 ORM을 사용 중이라면 꼭 Room으로 대체할 필요는 없습니다.

Room에서는 Annotation으로 로컬 스키마 정의가 가능합니다. (@Entity, @PrimaryKey)

RoomDatabase를 구현하여 추상 클래스를 만들면 Room에서는 자동으로 구현을 제공합니다.

 

데이터를 데이터베이스에 액세스하기 위해 데이터 액세스 객체 (DAO) 를 만듭니다.

Room 메서드에서 LiveData를 사용하면 데이터 변경시 자동으로 모든 Observer들에게 알리게 됩니다.

DAO를 정의했으면 데이터베이스 클래스에서 DAO를 참조 후 Repository 소스를 수정하면 됩니다.

단일 소스 Repository

다양한 REST API 엔드포인트는 흔히 동일한 데이터를 반환합니다.

일관선 확인 없이 Repository가 WebService 요청으로부터 응답을 있는 그대로 반환한다면 Repository 의 데이터 버전과 형식이 가장 최근에 호출된 엔드포인트에 종속되므로 UI에서 혼란스러운 정보를 표시할 수 있습니다.

 

그렇기에 Repository 구현에서는 데이터베이스에 WebService 응답을 저장합니다.

그러면 데이터베이스 변경시 LiveData 객체에 콜백이 트리거 됩니다.

 

이 모델을 사용하면 데이터베이스가 단일 소스 Repository 역할을 하며 앱의 다른 부분은 Repository를 사용하여 데이터베이스에 엑세스 합니다. 디스크 캐시 사용 여부와 상관없이 Repository에서 데이터 소스를 나머지 앱의 단일 소스 Repository 로 지정하는 것이 좋습니다.

진행 중인 작업 표시

SwipeRefresh 같은 사용 사례에서는 네트워크 작업시 UI 표시가 중요합니다.

다양한 이유로 데이터가 업데이트 될 수 있으므로 실제 데이터와 UI 작업을 분리하는 것이 좋습니다.

  • LiveData 유형의 객체를 반환하도록 변경하고, 이 객체에는 네트워크 작업 상태가 포함됩니다.
  • Repository 클래스에 데이터 새로고침 상태를 반환할 수 있는 다른 함수를 제공합니다.

각 구성요소 테스트

관심사 분리의 원칙을 따랐을 때 한 가지 중요한 이점은 테스트 가능성입니다.

  • UI 및 인터렉션 : Android UI Instrumentation Test 를 사용합니다. Espresso 라이브러리를 사용하는 것이 가장 좋으며, Mock으로 ViewModel을 제공할 수 있습니다. Activity나 Fragment는 ViewModel 하고만 통신하므로 이 클래스만으로도 충분히 UI 를 테스트 할 수 있습니다.

  • ViewModel : JUnit Test 를 사용하여 ViewModel 클래스를 테스트할 수 있습니다. Repository만 Mock 객체로 테스트하면 됩니다.

  • Repository : 역시 JUnit Test로 테스트할 수 있습니다. WebService난 DAO를 Mock 객체로 테스트하면 됩니다.

    • Repository가 올바른 WebService를 호출하는지
    • Repository가 데이터베이스에 결과를 저장하는지
    • 데이터가 캐시되고 최신인 경우 Respository가 불필요한 요청은 안만드는지
  • WebService와 DAO 는 모두 인터페이스이므로 더 복잡한 테스트를 위해 Mock이나 fake 구현을 만들 수 있습니다.

  • DAO : Instrumentation Test를 사용하여 테스트합니다. UI 구성요소가 필요하지 않으므로 빠르게 실행되며, 각 테스트에서 메모리 내에 데이터베이스를 만들어 테스트 부작용이 없게 합니다. (ex. 디스크의 DB 파일 변경)

    • Room에서 데이터베이스 구현을 지정하도록 허용하므로 SupportSQLiteOpenHelper 의 JUnit 구현을 제공하여 DAO를 테스트할 수 있습니다. 하지만 기기에서 실행 중인 SQLite 버전이 개발 PC의 SQLite 버전과 다를 수 있으므로 이 방법은 권장되지 않습니다.

  • WebService : 테스트에서 백엔드로 네트워크 호출하지 않도록 합니다. 웹 기반 테스트를 포함한 모든 테스트가 외부로부터 독립적이어야 합니다. MockWebServer 를 포함하여 여러 라이브러리를 활용하여 가짜 로컬 서버를 만들 수 있습니다.

  • 아티팩트 테스트 : 아키텍처 구성요소는 백그라운드 스레드를 제어할 수 있는 maven 아티팩트를 제공합니다.

    `androidx.arch.core:core-testing` 아티팩트에는 다음과 같은 JUnit 규칙이 있습니다.
    • InstantTaskExecutorRule : 호출 스레드에서 백그라운드 작업을 즉시 실행합니다.
    • CountingTaskExecutorRule : 아키텍처 구성요소의 백그라운드 작업을 기다립니다. Espresso와 연결할 수 있습니다.

권장사항

다음 권장사항은 필수는 아니지만 경험에 의한 권장사항을 따르는 경우 장기적으로 더 강력하고 테스트 및 유지관리가 쉬운 코드 베이스를 만들 수 있습니다.

  • Activity, Service, Broadcast Receiver와 같은 앱의 진입점을 데이터 소스로 지정하지 마세요.
  • 앱의 다양한 모듈 간 책임이 잘 정의된 경계를 만듭니다.
  • 각 모듈에서 가능하면 적게 노출합니다.
  • 각 모듈을 독립적으로 테스트하는 방법을 고려합니다.
  • 다른 앱과 차별되도록 앱의 고유한 핵심에 초점을 맞춥니다.
  • 가능한 한 관련성이 높은 최신 데이터를 보존합니다.
  • 하나의 데이터 소스를 단일 소스 저장소로 지정합니다.

추가: 네트워크 상태에 따른 노출 처리 로직

+ Recent posts