몇 달이 걸리더라도 나는 위 프로젝트의 모든 설계원리와 적용된 디자인패턴 등을 모두 이해하고, 가능하다면 contributor가 되고 싶기도 하여, 해당 프로젝트를 틈틈히 공부하기로 결정하였다.
해당 프로젝트는 android 공식 깃허브 저장소의 멀티 모듈 & Jetpack Compose 프로젝트이다.
엄청난 프로젝트인듯 하다...
우선 프로젝트를 clone 하여 내 로컬 (맥북 프로 m1 만세!)에서 열어보았다.
프로젝트 구조는 다음과 같았다.
이 앱의 아키텍처의 목표는 다음과 같다.
앱 아키텍처는 두 가지 레이어를 갖고 있다.
(그리고, 세 번째로는 현재 Domain Layer도 개발중이다.)
아키텍처는 단방향 데이터 흐름과 함께 리액티브 프로그래밍을 따르고 있다.
가장 근본적인 데이터 레이어의 주요 컨셉은 다음과 같다.
데이터 흐름(data flow)은 코틀린 플로우를 구현한 스트림(Stream)을 사용한다.
앱을 처음 빌드하고 실행하면, Now in Android 앱은 리모트 서버(Remote Server)로부터 뉴스 리소스 목록들을 불러온다.
한 번 목록들이 로딩되면, 이것은 유저들이 관심분야로 선택한 것에 따라 유저들에게 보여진다.
아래의 다이어그램은 위 과정에 대해서 이벤트가 발생하고 어떻게 관련 객체들로부터 데이터가 흘러가는지 보여준다.
필자 또한 이 그림을 여러 번 보면서 offline-first 앱의 흐름을 익혔다
이제부터는 각 과정에 대해서 어떤 일들이 발생하는 지 보여줄 것이다.
관련된 코드를 찾는 가장 쉬운 방법은 안드로이드 스튜디오를 열어서
⇧ SHIFT 키를 두 번 눌러서 코드 컬럼을 찾는것이다!
단계 | 설명 | 코드 |
1 | On app startup, a WorkManager job to sync all repositories is enqueued. | SyncInitializer.create
|
2 | The initial news feed state is set to Loading , which causes the UI to show a loading spinner on the screen.
|
Search for usages of NewsFeedUiState.Loading
|
3 | WorkManager executes the sync job which calls OfflineFirstNewsRepository to start synchronizing data with the remote data source.
|
SyncWorker.doWork
|
4 | OfflineFirstNewsRepository calls RetrofitNiaNetwork to execute the actual API request using Retrofit.
|
OfflineFirstNewsRepository.syncWith
|
5 | RetrofitNiaNetwork calls the REST API on the remote server.
|
RetrofitNiaNetwork.getNewsResources
|
6 | RetrofitNiaNetwork receives the network response from the remote server.
|
RetrofitNiaNetwork.getNewsResources
|
7 | OfflineFirstNewsRepository syncs the remote data with NewsResourceDao by inserting, updating or deleting data in a local Room database.
|
OfflineFirstNewsRepository.syncWith
|
8 | When data changes in NewsResourceDao it is emitted into the news resources data stream (which is a Flow).
|
NewsResourceDao.getNewsResourcesStream
|
9 | OfflineFirstNewsRepository acts as an intermediate operator on this stream, transforming the incoming PopulatedNewsResource (a database model, internal to the data layer) to the public NewsResource model which is consumed by other layers.
|
OfflineFirstNewsRepository.getNewsResourcesStream
|
10 | When ForYouViewModel receives the news resources it updates the feed state to Success . ForYouScreen then uses the news resources in the state to render the screen.
The screen shows the newly retrieved news resources (as long as the user has chosen at least one topic or author). |
Search for instances of NewsFeedUiState.Success
|
표를 반복해서 읽다보면 For You Screen에 대한 동작 구조가 대강 보인다.
데이터 레이어는 오프라인 우선 원칙에 의해 구현된다.
각각의 repository는 각자의 models를 가지고 있다.
예를 들어 TopicsRepository는 Topic 모델을 갖고 있고,
NewsRepository는 NewsResources 모델을 갖고 있다.
Repository는 다른 계층의 레이어에게 public하고, 그들은 앱 데이터에 접근하는 유일한 방법을 제공해준다. 또한 그 Repositories는 일반적으로 데이터를 읽거나 쓰거나 할 하나 혹은 그 이상의 메서드들을 제공해준다.
데이터는 data stream 형태로 노출된다. 이것은 각각의 repository 마다의 client는 데이터 변화에 항상 대비하고 있어야 한다는 뜻이다. 데이터는 스냅샷 형태로 방출되지도 않으며 왜냐하면 시간이 지난다면 그것들이 유효할지에 대한 보장이 없기 때문이다.
읽기는 로컬 저장소에서 수행되고, 따라서 이 과정 자체에서 에러가 난다기 보다는, 로컬 저장소와 원격 remote source를 합치거나 조정하는 과정에서 에러가 발생할 확률이 높다.
데이터를 쓰기 위해, repository는 suspend function을 제공한다!
그 실행들이 적절한 scope 내에서 수행될지에 대한 여부는 caller에게 달려있다.
repository는 아마도 하나 혹은 그 이상의 data sources에게 의존할 것이다.
예를들어 OfflineFirstTopicsRepository는 아래의 data sources에 의존함을 보여준다.
Name | Backed by | Purpose |
TopicsDao | Room/SQLite | Persistent relational data associated with Topics |
NiaPreferencesDataSource | Proto DataStore | Persistent unstructured data associated with user preferences, specifically which Topics the user is interested in. This is defined and modeled in a .proto file, using the protobuf syntax. |
NiaNetworkDataSource | Remote API accessed using Retrofit | Data for topics, provided through REST API endpoints as JSON. |
Repositories는 리모드 소스와 로컬 저장소의 데이터 싱크를 맞출 책임이 있다.
원격 Remote data로부터 한 번 데이터를 얻게 되면, 그것은 곧바로 로컬 저장소에 쓰이게 된다.
업데이트된 데이터는 로컬 저장소(Room)로부터 관련된 데이터 스트림으로 방출되며 대기하고 있던 client들에게 전달된다.
이러한 접근방식은 앱의 read와 write는 분리된 영역이고 서로 방해하지 않는다는 것을 보장한다.
이러한 데이터 동기화 과정에서 에러가 발생할 때, 지수 백오프 알고리즘이 적용될 수 있다.
이것은 SyncWorker를 통해 WorkManager에게 위임되고, Synchronizer 인터페이스를 구현하는 방식으로 진행된다.
프로젝트 내 OfflineFirstNewsRepository.syncWith
에서 데이터 동기화에 대한 예시 코드를 볼 수 있다.
UI 레이어 는 다음과 같이 구성되어 있다.
뷰모델은 레포지토리로부터 데이터 스트림을 전달 받고, 그것들을 UI state로 변형시킨다.
UI 요소들은 이 state에 반응하고, 사용자로 하여금 앱과 상호작용 하는 방법들을 제공한다.
이러한 상호작용은 event 형태로 전달되며, 그것들이 처리되는 곳인 뷰모델로 들어간다.
UI state는 인터페이스와 immutable 데이터 클래스를 사용하여 sealed hierarchy 형태로 모델링된다. 이러한 접근 방식은 다음을 보장한다.
예시 : News feed on For You screen
For You screen의 뉴스 피드(혹은 뉴스 피드 목록)는 NewsFeedUiState
를 사용하여 모델링 된다. 이것은 두 가지 가능한 구조를 만드는 sealed interface이다.
Lodaing
은 데이터가 로딩되고 있음을 나타낸다.Success
는 데이터가 성공적으로 로딩되었음을 나타낸다. Success state는 뉴스 리스트 목록을 포함한다.이 두가지 states들을 관리하는 feedState
는 ForYouScreen
composable로 들어간다.
뷰모델들은 하나 혹은 그 이상의 레포지토리들로부터 cold flows 형태로 데이터 스트림을 전달 받는다.
이것들은 UI state에 해당하는 하나의 single flow를 만들어내기 위해 함께 combined된다.
이러한 single flow는 stateIn을 사용하여 hot flow로 변환된다.
이러한 state flow의 변화는 UI 요소들로 하여금 flow부터 들어온 가장 최신의 state를 읽을 수 있도록 해준다.
예시 : Displaying followed topics and authors