5. Android App Architecture Data Layer
UI Layer에는 UI 관련 State, Logic이 포함되지만 데이터 영역에서는 Application Data 및 Business Logic(이하 비즈니스 로직)이 포함된다. 비즈니스 로직은 앱에 가치를 부여하는 요소로, 어플리케이션의 데이터 생성 저장, 변경 방식을 결정하는 실제 비즈니스 규칙으로 구성된다.
이렇게 관심사를 분리하면 데이터 영역을 여러 화면에서 사용하고, 앱의 여러 부분 간에 정보를 공유하고, 단위 테스트를 위해 UI 외부에 비즈니스 로직을 재현할 수 있다.
데이터 영역 아키텍처
데이터 영역은 0개부터 여러 개의 데이터 소스를 각각 포함할 수 있는 Repository(이하 저장소)로 구성된다. 앱에서 처리하는 다양한 유형의 데이터 별로 저장소 클래스를 만들어야 한다.
Ex) 영화 관련 데이터에는 MoviesRepository 클래스를 만들거나 관련 데이터에는 PaymentRepository 클래스를 만들 수 있다.
저장소 클래스에서 담당하는 작업은 다음과 같다.
1. 앱의 나머지 부분에 데이터 노출
2. 데이터 변경사항을 한 곳에 집중
3. 여러 데이터 소스 간의 충돌 해결
4. 앱의 나머지 부분에서 데이터 소스 추상화
5. 비즈니스 로직 포함
각 데이터 소스 클래스 파일, 네트워크 소스, 로컬 DB와 같은 하나의 데이터 소스만 사용해야 한다. 데이터 소스 클래스는 데이터 작업을 위해 어플리케이션과 시스템 간의 가교 역할을 한다.
계층 구조의 다른 레이어는 데이터 소스에 직접 액세스 하면 안된다. 데이터 영역의 진입점은 항상 저장소 클래스여야 한다.
State Holder 클래스 또는 UseCase 클래스에는 데이터 소스가 직접 종속항목으로 있어서는 안된다.
저장소 클래스를 진입점으로 사용하면 아키텍처의 다양한 레이어를 독립적으로 확장할 수 있다.
이 레이어에서 노출된 데이터는 변경 불가능해야 한다. 그래야 값을 일관되지 않은 상태로 만들 위험이 있는 다른 클래스에 의한 조작이 불가능해진다. 또한 변경 불가능한 데이터는 여러 스레드에서 안전하게 처리될 수 있다.
종속 항목 삽입 권장사항에 따라 저장소는 데이터 소스를 생성자의 종속항목으로 사용한다.
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) { /* ... */ }
API 노출
데이터 영역의 클래스는 일반적으로 원샷 생성, 조회, 업데이트 및 삭제(CRUD: Create, Read, Update, Delete) 호출을 실행하거나 시간 경과에 따른 데이터 변경사항에 관해 알림을 받는 함수를 노출한다. 데이터 영역은 다음과 같은 경우에 각 항목을 노출해야 한다.
1. 원샷 작업: 데이터 영역에서 Kotlin의 suspend 함수를 노출해야한다. 자바의 경우 데이터 영역에서 작업 결과 또는 RxJava, Single, Maybe, Completable 유형에 대한 콜백을 제공하는 함수를 노출해야 한다.
2. 시간 경과에 따른 데이터 변경사항에 관해 알림을 받으려면: 데이터 영역에서 Kotlin의 Flow를 노출해야 한다. 자바의 경우 데이터 영역에서 새 데이터 또는 RxJava Observable 또는 Flowable 유형을 내보내는 콜백을 노출해야 한다.
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) {
val data: Flow<Example> = ...
suspend fun modifyData(example: Example) { ... }
}
이름 지정 규칙
저장소 클래스의 이름은 담당하는 데이터의 이름을 따라 지정된다. 규칙은 다음과 같다.
데이터 유형 + 저장소
Ex: NewsRepository, MoviesRepository, PaymentRepository
데이터 소스 클래스의 이름은 담당하는 데이터와 사용하는 소스의 이름을 따라 지정된다. 규칙은 다음과 같다.
데이터 유형 + 소스 유형 + DataSource
데이터 유형의 경우 구현이 변경될 수 있으므로 좀 더 일반적인 Remote 또는 Local을 사용한다.
Ex: NewsRemoteDataSource, NewsLocalDataSource가 있다. 소스가 중요한 경우를 좀 더 구체적으로 지정하려면 소스 유형을 사용한다. 예를 들면 NewsNetworkDataSource 또는 NewsDiskDataSource가 있다.
구현 세부정보에 따라 데이터 소스의 이름을 지정하지 마라. (Ex: UserSharedPreferencesDataSource) 해당 데이터 소스를 사용하는 저장소가 데이터 저장 방법을 알 수 없다. 이 규칙을 따르면 데이터 소스의 구현을 변경하면서도 해당 소스를 호출하는 레이어에 영향을 주지 않을 수 있다.
여러 수준의 저장소
더 복잡한 비즈니스 요구사항이 포함된 일부 경우에는 저장소가 다른 저장소에 종속되어야 할 수 있다. 관련 데이터가 여러 데이터 소스의 집계이거나 책임이 다른 저장소 클래스에 캡슐화되어야 하기 때문일 수 있다.
Ex) 사용자 인증 데이터를 처리하는 저장소인 UserRepository는 요구사항을 충족하기 위해 LoginRepository 및 RegistrationRepository와 같은 다른 저장소에 종속될 수 있다.
정보 소스
각 저장소가 하나의 정보 소스를 정의하는 것이 중요하다. 정보 소스는 항상 일관되고 정확하며 최신 상태인 데이터를 포함한다. 실제로 저장소에서 노출되는 데이터는 항상 정보 소스에서 직접 가져온 데이터여야 한다.
정보 소스는 데이터 소스(Ex: Database)이거나 저장소에 포함될 수 있는 메모리 내 캐시일 수 있다. 저장소는 서로 다른 데이터 소스를 결합하고 데이터 소스 간의 잠재적인 충돌을 해결하여 정기적으로 또는 사용자 입력 이벤트에 따라 정보 소스를 업데이트 한다.
앱의 저장소마다 정보 소스가 다를 수 있다. 예를 들어 LoginRepository 클래스는 캐시를 정보 소스로 사용하고 PaymentRepository 클래스는 네트워크 데이터 소스를 사용할 수 있다.
오프라인 우선 지원을 제공하려면 데이터베이스와 같은 로컬 데이터 소스로 사용하는 것이 좋다.
스레딩
데이터 소스와 저장소 호출은 기본 스레드에서 호출하기에 안전하도록 기본 안전성이 보장되어야 한다. 이러한 클래스는 장기 실행 ㅏ단 작업을 실행할 때 로직 실행을 적절한 스레드로 이동한다.
Ex) 데이터 소스가 파일에서 읽거나 저장소가 큰 목록에서 비용이 많이 드는 필터링을 수행할 때 기본 안전성이 보장되어야 한다.
대부분의 데이터 소스는 이미 Room 또는 Retrofit에서 제공하는 정지 메서드 호출과 같은 기본 안전성을 갖춘 API를 제공한다. API를 사용할 수 있게 되면 저장소에서 API를 활용할 수 있다.
수명 주기
데이터 영역에서 클래스 인스턴스는 가비지 컬렉션 루트에서 연결할 수 있는 한 메모리에 남아 있다. 이는 대개 앱의 다른 객체에서 참조된다.
클래스에 메모리 내 데이터가 포함된 경우(Ex: Cache) 특정 기간 동안 해당 클래스의 동일한 인스턴스를 재사용하고자 할 수 있다. 이를 클래스 인스턴스의 수명 주기라고도 한다.
클래스의 책임이 전체 어플리케이션에 중요한 경우 해당 클래스의 인스턴스 범위를 Application 클래스로 지정할 수 있다. 이렇게 하면 인스턴스가 어플리케이션의 수명 주기를 따르게 된다. 또는 앱의 특정 흐름(Ex: 등록 또는 로그인 Flow)에서만 동일한 인스턴스를 재사용해야 하는 경우 흐름의 수명 주기를 소유한 클래스로 인스턴스 범위를 지정해야 한다. 예를 들어 메모리 내 데이터가 포함된 RegistrationRepository 범위를 RegistrationActivity 또는 등록 흐름의 탐색 그래프로 지정할 수 있다.
각 인스턴스의 수명 주기는 앱 내에서 종속 항목을 제공하는 방법을 결정할 때 중요한 요소이다. 종속 항목이 관리되고 종속 항목 컨테이너로 범위가 지정될 수 있는 종속 항목 삽입 권장사항을 따르는 것이 좋다.
대표 비즈니스 모델
데이터 영역에서 노출하려는 데이터 모델은 다양한 데이터 소스에서 가져오는 정보의 하위 집합일 수 있다. 네트워크 및 로컬의 다양한 데이터 소스가 어플리케이션에 필요한 정보만 반환하는 것이 좋으나 실제 이런 경우는 많지 않다.
예를들어 기사 정보 뿐만 아니라 수정 기록, 사용자 댓글, 일부 메타데이터도 반환하는 News API 서버가 있다고 하자.
data class ArticleApiModel(
val id: Long,
val title: String,
val content: String,
val publicationDate: Date,
val modifications: Array<ArticleApiModel>,
val comments: Array<CommentApiModel>,
val lastModificationDate: Date,
val authorId: Long,
val authorName: String,
val authorDateOfBirth: Date,
val readTimeMin: Int
)
화면에 기사 콘텐츠와 작성자에 관한 기본 정보만 표시하므로 앱은 기사에 관한 많은 정보를 필요로 하지 않는다. 모델 클래스로 분리하고 저장소에서 계층 구조의 다른 레이어에 필요한 데이터만 노출하도록 하는 것이 좋다.
예를 들어 다음은 Article 모델 클래스를 Domain 및 UI Layer에 노출하기 위해 네트워크에서 ArticleApiModel을 다듬는 방법이다.
data class Article(
val id: Long,
val title: String,
val content: String,
val publicationDate: Date,
val authorName: String,
val readTimeMin: Int
)
모델 클래스를 분리하면 다음과 같은 이점이 있다.
1. 필요한 수준으로 데이터를 줄여 앱 메모리를 절약한다.
2. 앱에서 사용하는 데이터 유형에 맞게 외부 데이터 유형을 조절한다. 예를 들어 앱은 날짜를 나타내는 데 다른 데이터 유형을 사용할 수 있다.
3. 이를 통해 관심사를 더 잘 분리할 수 있다. 예를 들어 모델 클래스가 미리 정의된 경우 대규모 팀원이 기능의 네트워크 레이어와 UI 레이어에서 개별적으로 작업할 수 있다.
이 방식을 확장하고 앱 아키텍처의 다른 부분(Ex: DataSource Class , ViewModel)에서도 별도의 모델 클래스를 정의할 수 있다.
그러나 이를 위해서는 적절하게 문서화하고 테스트해야 하는 추가 클래스 및 로직을 정의해야 한다. 최소한 데이터 소스가 앱의 나머지 부분에서 예상하는 데이터와 일치하지 않는 데이터를 수신하는 경우에는 새 모델을 만드는 것이 좋다.
데이터 작업 유형
데이터 영역에서 중요도에 따라 다양한 유형의 작업(Ex: UI 지향, 앱 지향, 비즈니스 지향 작업)을 처리할 수 있다.
UI 지향 작업
UI지향 작업은 사용자가 특정 화면에 있을 때 관련이 있고 사용자가 화면에서 멀어지면 취소된다. 예를 들어 데이터베이스에서 얻은 일부 데이터를 표시한다.
UI지향 작업은 일반적으로 UI 레이어에 의해 트리거되며 호출자의 수명 주기(Ex: ViewModel의 수명 주기)를 따른다.
앱 지향 작업
앱 지향작업은 앱이 열려 있는 한 관련이 없다. 앱이 닫히거나 프로세스가 종료되면 이러한 작업은 취소된다.
Ex) 네트워크 요청의 결과를 필요에 따라 나중에 사용할 수 있도록 캐시하는 경우가 있다.
비즈니스 지향 작업
비즈니스 지향 작업은 취소할 수 없다. 프로세스 종료 후에도 유지된다.
Ex) 사용자가 프로필에 게시하고 싶은 사진 업로드를 완료하는 작업이 있다.
비즈니스 지향 작업의 경우 WorkManager를 사용하는 것이 좋다.
오류 노출
저장소 및 데이터 소스와의 상호작용은 성공하거나 실패 시 예외를 발생시킬 수 있다. 코루틴과 흐름의 경우 Kotlin의 기본 제공 오류 처리 메커니즘을 사용해야 한다. suspend 함수에 의해 트리거 될 수 있는 오류의 경우 적절한 try/catch 블록을 사용하며 Flow에서는 catch 연산자를 사용한다. 이 접근 방식을 사용하면 데이터 영역을 호출할 때 UI Layer가 예외를 처리해야 한다.
데이터 영역은 다양한 유형의 오류를 이해하고 처리하며 Custom Exception(Ex: UserNotAuthenticatedException)를 사용하여 이를 노출할 수 있다.
일반적인 작업
이번에는 Android 앱에서 일반적으로 사용되는 특정 작업을 실행하기 위해 데이터 영역을 사용하고 설계하는 방법의 예를 본다.
네트워크 요청
네트워크 요청은 Android 앱에서 실행할 수 있는 가장 일반적인 작업 중 하나이다. 뉴스 앱은 네트워크에서 가져온 최신 뉴스를 사용자에게 표시해야 한다. 따라서 앱에서 네트워크 작업을 관리하기 위한 데이터 소스 클래스 NewsRemoteDataSource가 필요하다. 앱의 나머지 부분에 정보를 노출하기 위해 뉴스 데이터에 관한 작업을 처리하는 새로운 저장소 NewsRepository를 만든다.
요구사항은 사용자가 화면을 열 때 항상 최신 뉴스를 업데이트 하도록 하는 것이다 .따라서 이는 UI 지향 작업이다.
1. 데이터 소스 만들기
데이터 소스는 최신 뉴스를 반환하는 함수, 즉 ArticleHeadline 인스턴스 목록을 노출해야 한다. 데이터 소스는 네트워크에서 최신 뉴스를 가져오는 기본 안정성을 갖춘 방법을 제공해야 한다. 이 경우 작업을 실행할 CoroutineDispatcher 또는 Executor에 종속 항목을 가져와야 한다.
네트워크 요청은 새로운 fetchLatesNews() 메소드에서 처리되는 원샷 호출이다.
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val ioDispatcher: CoroutineDispatcher
) {
/**
* Fetches the latest news from the network and returns the result.
* This executes on an IO-optimized thread pool, the function is main-safe.
*/
suspend fun fetchLatestNews(): List<ArticleHeadline> =
// Move the execution to an IO-optimized thread since the ApiService
// doesn't support coroutines and makes synchronous requests.
withContext(ioDispatcher) {
newsApi.fetchLatestNews()
}
}
}
// Makes news-related network synchronous requests.
interface NewsApi {
fun fetchLatestNews(): List<ArticleHeadline>
}
NewsApi 인터페이스는 네트워크 API 클라이언트의 구현을 숨긴다. 인터페이스가 Retrofit 또는 HttpURLConnection의 지원을 받는지에 따라 달라지지 않는다. 인터페이스에 의존하면 앱에서 API 구현을 교체할 수 있다.
2. 저장소 만들기
이 작업의 저장소 클래스에는 추가 로직이 필요하지 않으므로 NewsRepository는 네트워크 데이터 소스의 프록시 역할을 한다.
// NewsRepository is consumed from other layers of the hierarchy.
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
suspend fun fetchLatestNews(): List<ArticleHeadline> =
newsRemoteDataSource.fetchLatestNews()
}
메모리 내 데이터 캐싱 구현
뉴스 앱에 새로운 요구사항이 도입되었다고 가정해 보자. 사용자가 화면을 열면 이전에 요청이 생성된 경우 캐시된 뉴스가 사용자에게 표시되어야 한다. 그러지 않으면 앱이 최신 뉴스를 가져오기 위해 네트워크 요청을 해야 한다.
새로운 요구사항이 있으므로 앱은 사용자가 앱을 열고 있는 동안 메모리에 최신 뉴스를 보존해야 한다. 따라서 이는 앱 지향 작업이다.
1. 캐시
사용자가 앱에 있는 동안 메모리 내 데이터 캐싱을 추가하여 데이터를 보존할 수 있다. 캐시는 사용자가 앱에 있는 한 특정 시간 동안 메모리에 일부 정보를 저장하기 위해 실행된다. 캐시 구현은 다양한 형태를 취할 수 있다. 간단한 변경 가능 변수부터 여러 스레드에서 읽기/쓰기 작업을 금지하는 더욱 정교한 클래스에 이르기까지 다양할 수 있다. UseCase에 따라 저장소 또는 데이터 소스 클래스 내에 캐싱을 구현할 수 있다.
2. 네트워크 요청 결과 캐시
편의상 NewsRepository는 변경 가능한 변수를 사용하여 최신 뉴스르 캐시한다. 여러 스레드에서 읽기 및 쓰기를 금지하기 위해 Mutex가 사용된다.
다음 구현은 Mutex로 쓰기가 금지된 저장소의 변수에 최신 뉴스 정보를 캐시한다. 네트워크 요청 결과가 성공하면 데이타기 latestNews 변수에 할당된다.
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
// Mutex to make writes to cached values thread-safe.
private val latestNewsMutex = Mutex()
// Cache of the latest news got from the network.
private var latestNews: List<ArticleHeadline> = emptyList()
suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
if (refresh || latestNews.isEmpty()) {
val networkResult = newsRemoteDataSource.fetchLatestNews()
// Thread-safe write to latestNews
latestNewsMutex.withLock {
this.latestNews = networkResult
}
}
return latestNewsMutex.withLock { this.latestNews }
}
}
3. 작업을 화면보다 길게 유지
네트워크 요청이 진행되는 동안 사용자가 화면에서 벗어나면 취소되고 결과가 캐시되지 않는다. NewsRepository는 이 로직을 실행하는 데 호출자의 CoroutineScope를 사용해서는 안된다. 대신 NewsRepository는 수명 주기에 연결된 CoroutineScope를 사용해야 한다.
최신 뉴스를 가져오는 작업은 앱 지향 작업이어야 한다.
종속 항목 삽입 권장사항을 따르려면 NewsRepository는 자체 CoroutineScope를 만드는 대신 생성자의 매개변수로 범위를 수신해야 한다. 저장소는 대부분의 작업을 백그라운드 스레드에서 해야 하므로 CoroutineScope를 Dispatchers.Default 또는 자체 스레드 풀로 구성해야 한다.
class NewsRepository(
...,
// This could be CoroutineScope(SupervisorJob() + Dispatchers.Default).
private val externalScope: CoroutineScope
) { ... }
NewsRepository는 외부 CoroutineScope를 사용하여 앱 지향 작업을 실행할 준비가 되어 있으므로 데이터 소스 호출을 실행하고 그 범위에서 시작된 새 코루틴으로 결과를 저장해야 한다.
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource,
private val externalScope: CoroutineScope
) {
/* ... */
suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
return if (refresh) {
externalScope.async {
newsRemoteDataSource.fetchLatestNews().also { networkResult ->
// Thread-safe write to latestNews.
latestNewsMutex.withLock {
latestNews = networkResult
}
}
}.await()
} else {
return latestNewsMutex.withLock { this.latestNews }
}
}
}
async는 외부 범위에서 코루틴을 시작하는 데 사용된다. 네트워크 요청이 다시 발생하고 결과가 캐시에 저장될 때까지 정지하기 위해 await가 새 코루틴에서 호출된다. 그때 사용자가 여전히 화면에 있다면 최신 뉴스가 표시된다. 사용자가 화면에 벗어나면 await가 취소되지만 async 내부의 로직은 계속 실행된다.
데이터 저장 및 디스크에서 가져오기
북마크한 뉴스와 사용자 환경서렁과 같은 데이터를 저장하려 한다고 가정해보자. 이러한 유형의 데이터는 사용자가 네트워크에 연결되어 있지 않더라도 프로세스가 종료된 후에도 남아 있어 엑세스 할 수 있어야 한다.
작업 중인 데이터가 프로세스 중단 후에도 유지되어야 하는 경우 다음 방법 중 하나로 데이터를 디스크에 저장해야 한다.
1. 쿼리해야 하거나 참조 무결성이 필요하거나 부분 업데이트가 필요한 대규모 데이터 세트의 경우 Room 데이터베이스에 데이터를 저장한다. 뉴스 앱 예시에서는 뉴스 기사나 작성자를 데이터베이스에 저장할 수 있다.
2. 쿼리하거나 부분적으로 업데이트하지 않고 검색 및 설정해야 하는 소규모 데이터 세트에는 DataStore를 사용한다. 뉴스 앱 예시에서 사용자의 기본 날짜 형식 또는 기타 표시 환경설정은 DataStore에 저장할 수 있다.
3. JSON 객체와 같은 데이터 청크의 경우 파일을 사용한다.
정보 소스 에서 언급했듯이 각 데이터 소스는 하나의 소스에서만 작동하며 특정 데이터 유형(Ex: News, Authors, NewsAndAuthors, UserPreferences)에 대응한다. 데이터 소스를 사용하는 클래스는 데이터가 저장되는 방식(Ex: 데이터베이스 또는 파일)을 알 수 없다.
데이터 소스로 사용되는 Room
각 데이터 소스는 특정 유형의 데이터에 관해 하나의 소스만 사용해야 하므로, Room 데이터 소스는 데이터 엑세스 객체(DAO) 또는 데이터베이스 자체를 매개변수로 수신할 수 있다.
Ex) NewsLocalDataSource는 NewsDao 인스턴스를 매개변수로 사용하고 AuthorsLocalDataSource는 AuthorsDao 인스턴스를 사용할 수 있다.
추가 로직이 필요하지 않은 경우 테스트에서 쉽게 대체할 수 있는 인터페이스이므로 DAO를 저장소에 직접 삽입할 수 있다.
데이터 소스로 사용되는 Datastore
DataStore는 사용자 설정과 같은 Key-Value 쌍을 저장하는데 적합하다.
Ex) 시간 형식, 알림, 환경설정, 사용자가 뉴스 항목을 읽은 후 표시하거나 숨길지 여부 등이 있다.
다른 객체와 마찬가지로 Datastore가 지원하는 데이터 소스에는 특정 유형이나 앱의 특정 부분에 해당하는 데이터가 포함되어야 한다. DataStore 읽기는 값이 업데이트될 때마다 방출되는 흐름으로 노출되므로 더욱 그렇다. 따라서 관련 환경설정을 동일한 Datastore에 저장해야 한다.
Ex) 알림 관련 환경설정만 처리하는 NotificationDataStore와 뉴스 화면 관련 환경설정만 처리하는 NewsPreferenceDataStore가 있을 수 있다. 이렇게 하면 업데이트 범위를 더 잘 지정할 수있다.
newsScreenPreferenceDataStore.data 흐름이 화면과 관련된 환경설정이 변경될 때만 발생하기 때문이다. 또한 객체의 수명 주기는 뉴스 화면이 표시되어 있는 동안에만 표시될 수 있으므로 더 짧을 수 있다.
데이터 소스로 사용되는 파일
JSON 객체나 비트맵과 같은 큰 객체로 작업할 때는 File 객체로 작업하고 스레드 전환을 처리해야 한다.
WorkManager를 사용하여 작업 예약
뉴스 앱에 또 다른 요구사항이 도입되었다고 가정해 보자. 이 앱은 기기가 충전되고 무제한 네트워크에 연결되어 있는 한 최신 뉴스를 정기적으로 자동으로 가져오는 옵션을 사용자에게 제공해야 한다. 따라서 비즈니스 지향작업이 된다.
이렇게 하면 사용자가 앱을 열 때 기기가 연결되지 않아도 사용자가 최신 뉴스를 볼 수 있다.
WorkManager를 사용하면 신뢰할 수 있는 비동기 작업을 쉽게 예약할 수 있으며 제약 조건을 관리할 수 있다. 영구 작업에 권장되는 라이브러리이다. 위 정의된 작업을 실행하기 위해 Worker 클래스인 FetchLatestNewsWorker가 생성된다. 이 클래스는 최신 뉴스를 가져와서 디스크에 캐시하기 위해 NewsRepository를 종속 항목으로 사용한다.
class RefreshLatestNewsWorker(
private val newsRepository: NewsRepository,
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result = try {
newsRepository.refreshLatestNews()
Result.success()
} catch (error: Throwable) {
Result.failure()
}
}
이 작업 유형의 비즈니스 로직은 자체 클래스에 캡슐화되고 별도의 데이터 소스로 처리되어야 한다. 그러면 WorkManager는 모든 제약 조건이 충족될 때 작업이 백그라운드 스레드에서 실행되도록 해야 한다. 이 패턴을 준수하면 필요에 따라 다른 환경의 구현을 신속하게 교체할 수 있다.
이 예시에서는 뉴스 관련 작업이 NewsRepository에서 호출되어야 한다. 그럼 새 데이터 소스를 NewsTaskDataSource 종속 항목으로 삼게 되며 다음과 같이 구현된다.
private const val REFRESH_RATE_HOURS = 4L
private const val FETCH_LATEST_NEWS_TASK = "FetchLatestNewsTask"
private const val TAG_FETCH_LATEST_NEWS = "FetchLatestNewsTaskTag"
class NewsTasksDataSource(
private val workManager: WorkManager
) {
fun fetchNewsPeriodically() {
val fetchNewsRequest = PeriodicWorkRequestBuilder<RefreshLatestNewsWorker>(
REFRESH_RATE_HOURS, TimeUnit.HOURS
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED)
.setRequiresCharging(true)
.build()
)
.addTag(TAG_FETCH_LATEST_NEWS)
workManager.enqueueUniquePeriodicWork(
FETCH_LATEST_NEWS_TASK,
ExistingPeriodicWorkPolicy.KEEP,
fetchNewsRequest.build()
)
}
fun cancelFetchingNewsPeriodically() {
workManager.cancelAllWorkByTag(TAG_FETCH_LATEST_NEWS)
}
}
이러한 유형의 클래스는 NewsTasksDataSource 또는 PaymentsTaskDataSource와 같이 책임이 있는 데이터에 따라 이름이 지정된다. 특정 데이터 유형과 관련된 모든 작업은 동일한 클래스에 캡슐화되어야 한다.
앱을 시작할 때 작업을 트리거해야 하는 경우 Initializer에서 저장소를 호출하는 앱 시작 라이브러리를 사용하여 WorkManager 요청을 트리거하는 것이 좋다.