관리 메뉴

나만을 위한 블로그

[Android] 앱 아키텍처 - 데이터 레이어란? 본문

Android

[Android] 앱 아키텍처 - 데이터 레이어란?

참깨빵위에참깨빵 2024. 5. 12. 22:49
728x90
반응형

https://developer.android.com/topic/architecture/data-layer?hl=ko

 

데이터 레이어  |  Android 개발자  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 데이터 레이어 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. UI 레이어에는 UI 관련 상태 및 UI 로직이

developer.android.com

 

UI 레이어에는 UI 관련 상태, UI 로직이 포함되지만 데이터 레이어에는 앱 데이터, 비즈니스 로직이 포함된다.

비즈니스 로직은 앱에 가치를 부여하는 요소로 앱 데이터 생성, 저장, 변경 방식을 결정하는 실제 비즈니스 규칙으로 구성된다.

이렇게 관심사를 분리하면 데이터 레이어를 여러 화면에서 사용하고 앱의 여러 부분 간에 정보를 공유하고 단위 테스트를 위해 UI 외부에서 비즈니스 로직을 재현할 수 있다.

 

데이터 레이어 아키텍처

 

데이터 레이어는 0개부터 여러 개의 데이터 소스를 각각 포함할 수 있는 레포지토리로 구성된다.

앱에서 처리하는 여러 유형의 데이터별로 레포지토리 클래스를 만들어야 한다.

영화 관련 데이터에는 MoviesRepository 클래스를 만들거나 결제 관련 데이터에는 PaymentsRepository 클래스를 만들 수 있다.

 

 

레포지토리 클래스에서 담당하는 작업은 아래와 같다.

 

  • 앱의 나머지 부분에 데이터 노출
  • 데이터 변경사항을 한 곳에 집중
  • 여러 데이터 소스 간의 충돌 해결
  • 앱의 나머지 부분에서 데이터 소스 추상화
  • 비즈니스 로직 포함

 

각 데이터 소스 클래스는 파일, 네트워크 소스, 로컬 DB 같은 하나의 데이터 소스만 써야 한다. 데이터 소스 클래스는 데이터 작업을 위한 앱, 시스템 간의 가교 역할을 한다.

계층 구조의 다른 레이어는 데이터 소스에 직접 접근해선 안 된다. 데이터 레이어의 진입점은 항상 레포지토리 클래스여야 한다. 상태 홀더 클래스 또는 유스케이스 클래스의 경우 데이터 소스가 직접 종속 항목으로 있어선 안 된다.

레포지토리 클래스를 진입점으로 쓰면 아키텍처의 여러 레이어를 독립적으로 확장할 수 있다.

 

이 레이어에서 노출된 데이터는 변경 불가능해야 한다. 그래야 값을 일관되지 않은 상태로 만들 위험이 있는 다른 클래스에 의한 조작이 불가능해진다. 또한 변경 불가능한 데이터는 여러 쓰레드에서 안전하게 처리될 수 있다. 종속 항목 삽입 권장사항에 따라 레포지토리는 데이터 소스를 생성자의 종속 항목으로 사용한다.

 

class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) { /* ... */ }

 

레포지토리가 단일 데이터 소스만 포함하고 다른 레포지토리에 종속되지 않는 경우 레포지토리, 데이터 소스의 책임을 레포지토리 클래스에 병합하는 경우가 많다. 이 경우 레포지토리가 최신 앱에서 다른 소스의 데이터를 처리해야 한다면 기능을 분할해야 한다.

 

API 노출

 

데이터 레이어의 클래스는 일반적으로 원샷 생성, 조회, 업데이트, 삭제 호출을 실행하거나 시간 경과에 따른 데이터 변경사항에 대해 알림을 받는 함수를 노출한다.

데이터 레이어는 아래 경우에 각 항목을 노출해야 한다.

 

  • 원샷 작업 : 데이터 레이어에서 코틀린의 정지 함수를 노출해야 한다
  • 시간 경과에 따른 데이터 변경사항에 대해 알림 받기 : 데이터 레이어에서 코틀린 Flow를 노출해야 한다

 

class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) {

    val data: Flow<Example> = ...

    suspend fun modifyData(example: Example) { ... }
}

 

이름 지정 규칙

 

이 가이드에서 레포지토리 클래스명은 담당하는 데이터명에 따라 지정된다. 규칙은 아래와 같다.

 

데이터 유형 + Repository

 

예를 들어 NewsRepository, MoviesRepository, PaymentsRepository다.

 

데이터 소스 클래스의 이름은 담당하는 데이터, 사용하는 소스 이름을 따라서 지정된다. 규칙은 아래와 같다.

 

데이터 유형 + 소스 유형 + DataSource

 

데이터 유형은 구현이 바뀔 수 있어서 좀 더 일반적인 Remote 또는 Local을 사용한다.

예시로 NewsRemoteDataSource, NewsLocalDataSource가 있다. 소스가 중요한 경우를 좀 더 구체적으로 지정하려면 소스 유형을 사용한다. NewsNetworkDataSource 또는 NewsDiskDataSource가 있다.

 

구현 세부정보에 따라 데이터 소스 이름을 지정하지 않는다(UserSharedPreferencesDataSource 등). 해당 데이터 소스를 사용하는 레포지토리가 데이터 저장 방법을 알 수 없다.

이 규칙을 따르면 데이터 소스 구현을 변경(쉐어드에서 DataStore로 이전 등)하면서도 해당 소스를 노출하는 레이어에 영향을 주지 않을 수 있다.

 

여러 수준의 레포지토리

 

더 복잡한 비즈니스 요구사항이 포함된 일부 경우엔 레포지토리가 다른 레포지토리에 종속돼야 할 수 있다.

관련 데이터가 여러 데이터 소스의 집계거나 책임이 다른 레포지토리 클래스에 캡슐화돼야 하기 때문일 수 있다.

사용자 인증 데이터를 처리하는 UserRepository는 요구사항을 충족하기 위해 LoginRepository, RegistrationRepository 같은 다른 레포지토리에 종속될 수 있다.

 

 

다른 레포지토리 클래스에 종속된 레포지토리 클래스명을 manager라고 지칭하는 개발자가 많다. 원하는 경우 이 이름 지정 규칙을 쓸 수 있다.

 

정보 소스

 

각 레포지토리가 하나의 정보 소스를 정의하는 게 중요하다. 정보 소스는 항상 일관되고 정확하며 최신 상태인 데이터를 포함한다. 실제로 레포지토리에서 노출되는 데이터는 항상 정보 소스에서 직접 가져온 데이터여야 한다.

정보 소스는 데이터 소스(DB 등)거나 레포지토리에 포함될 수 있는 메모리 내 캐시일 수 있다. 레포지토리는 서로 다른 데이터 소스를 결합하고 데이터 소스 간의 잠재적인 충돌을 해결해서 정기적으로 or 사용자 입력 이벤트에 따라 정보 소스를 업데이트한다.

앱 레포지토리마다 정보 소스가 다를 수 있다. LoginRepository 클래스는 캐시를 정보 소스로 사용하고, PaymentsRepository는 네트워크 데이터 소스를 쓸 수 있다. 오프라인 우선 지원을 제공하려면 DB 같은 로컬 데이터 소스를 정보 소스로 쓰는 게 좋다.

 

쓰레딩

 

데이터 소스와 레포지토리 호출은 메인 쓰레드에서 호출하기에 안전하도록 기본 안전성이 보장돼야 한다. 이런 클래스는 장기 실행 차단 작업을 실행할 때 로직 실행을 적절한 쓰레드로 이동한다.

데이터 소스가 파일에서 읽거나 레포지토리가 큰 목록에서 비용이 많이 드는 필터링을 수행할 때 기본 안전성이 보장돼야 한다.

대부분의 데이터 소스는 이미 Room, 레트로핏, Ktor에서 제공하는 정지 함수 호출 같은 기본 안전성을 갖춘 API를 제공한다. API를 쓸 수 있게 되면 레포지토리에서 API를 활용할 수 있다.

 

생명주기

 

데이터 레이어의 클래스 인스턴스는 가비지 컬렉션 루트에서 연결할 수 있는 한 메모리에 남아 있다. 이는 대개 앱의 다른 객체에서 참조된다.

클래스에 메모리 내 데이터가 포함된 경우(캐시 등) 특정 기간 동안 해당 클래스의 동일 인스턴스를 재사용하고자 할 수 있다. 이를 클래스 인스턴스의 생명주기라고도 한다.

클래스 책임이 전체 앱에 중요한 경우 해당 클래스의 인스턴스 범위를 Application 클래스로 지정할 수 있다. 이렇게 하면 인스턴스가 앱 생명주기를 따르게 된다.

또는 앱의 특정 흐름(회원가입 or 로그인 흐름)에서만 같은 인스턴스를 재사용해야 할 경우 Flow의 생명주기를 소유한 클래스로 인스턴스 범위를 지정해야 한다. 메모리 내 데이터가 포함된 RegistrationRepository 범위를 RegistrationActivity 또는 회원가입 흐름의 탐색 그래프로 지정할 수 있다.

각 인스턴스의 생명주기는 앱 내에서 종속 항목을 제공할 방법을 결정할 때 중요한 요소다. 종속 항목이 관리되고 종속 항목 컨테이너로 범위가 지정될 수 있는 종속 항목 삽입 권장사항을 따르는 게 좋다.

 

대표 비즈니스 모델

 

데이터 레이어에서 노출하려는 데이터 모델은 여러 데이터 소스에서 가져오는 정보의 하위 집합일 수 있다. 네트워크 및 로컬의 여러 데이터 소스가 앱에 필요한 정보만 리턴하는 게 좋지만 실제 이런 경우는 많지 않다.

기사 정보와 수정 기록, 유저 댓글, 일부 메타데이터도 리턴하는 뉴스 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 모델 클래스를 도메인, UI 레이어에 노출하기 위해 네트워크에서 ArticleApiModel을 다듬는 방법이다.

 

data class Article(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val authorName: String,
    val readTimeMin: Int
)

 

모델 클래스를 분리하면 아래 이점이 있다.

 

  • 필요 수준으로 데이터를 줄여 앱 메모리를 절약
  • 앱에서 쓰는 데이터 유형에 맞게 외부 데이터 유형 조정. 예를 들어 날씨를 나타내는 데 다른 데이터 유형을 쓸 수 있다
  • 이를 통해 관심사를 더 잘 분리할 수 있다. 모델 클래스가 미리 정의된 경우 대규모 팀원이 기능의 네트워크 레이어, UI 레이어에서 개별적으로 작업할 수 있다.

 

이 방식을 확장하고 앱 아키텍처의 다른 부분(데이터 소스 클래스, 뷰모델)에서도 별도의 모델 클래스를 정의할 수 있다.

그러나 이를 위해선 잘 문서화하고 테스트해야 하는 추가 클래스, 로직을 정의해야 한다. 최소한 데이터 소스가 앱의 나머지 부분에서 예상하는 데이터와 일치하지 않는 데이터를 받는 경우엔 새 모델을 만드는 게 좋다.

 

데이터 작업 유형

 

UI 지향 작업

 

유저가 특정 화면에 있을 때만 관련 있고 화면에서 멀어지면 취소된다. 일반적으로 UI 레이어에 의해 트리거되고 호출자의 생명주기(뷰모델의 생명주기 등)를 따른다.

 

앱 지향 작업

 

앱이 열려 있는 한 관련이 있다. 앱이 닫히거나 프로세스가 종료되면 이 작업은 취소된다.

일반적으로 Application 클래스 또는 데이터 레이어의 생명주기를 따른다.

 

비즈니스 지향 작업

 

이 작업은 취소할 수 없다. 프로세스 종료 후에도 유지된다. 이 작업은 WorkManager를 쓰는 게 좋다.

 

오류 노출

 

저장소, 데이터 소스와의 상호작용은 성공하거나 실패 시 예외를 발생시킬 수 있다.

코루틴, Flow의 경우 코틀린의 기본 오류 처리 메커니즘을 써야 한다. 정지 함수로 트리거될 수 있는 오류는 적절한 경우 try-catch 블록을 사용하고 Flow에선 catch 연산자를 쓴다. 이 접근 방식을 사용하면 데이터 레이어 호출 시 UI 레이어가 예외를 처리해야 한다.

 

일반적인 작업

 

안드로이드 앱에서 일반적으로 쓰이는 특정 작업을 실행하기 위해 데이터 레이어를 사용, 설계하는 방법의 예를 확인한다.

가이드 앞에서 언급한 뉴스 앱을 기반으로 한다.

 

네트워크 요청

 

안드로이드 앱에서 실행할 수 있는 가장 일반적인 작업 중 하나다. 뉴스 앱은 네트워크에서 가져온 최신 뉴스를 유저에게 표시해야 한다. 따라서 앱에는 네트워크 작업을 관리하기 위한 데이터 소스 클래스 NewsRemoteDataSource가 필요하다.

앱 나머지 부분에 정보를 노출하기 위해 뉴스 데이터 관련 작업을 처리하는 NewsRepository를 만든다.

요구사항은 유저가 화면을 열 때 항상 최신 뉴스를 업데이트하게 하는 것이다. 따라서 이는 UI 지향 작업이다.

 

데이터 소스 만들기

 

데이터 소스는 최신 뉴스를 리턴하는 함수 ArticleHeadline 인스턴스 목록을 노출해야 한다. 데이터 소스는 네트워크에서 최신 뉴스를 가져오는 기본 안전성을 갖춘 메서드를 제공해야 한다.

이 경우 작업을 실행할 CoroutineDispatcher 또는 Executor에 종속 항목을 가져와야 한다.

네트워크 요청은 새로운 fetchLatestNews()에서 처리되는 원샷 호출이다.

 

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 클라이언트의 구현을 숨긴다. 인터페이스가 레트로핏 또는 HttpURLConnection의 지원 여부에 따라 달라지지 않는다. 인터페이스에 의존하면 앱에서 API 구현을 교체할 수 있다.

 

레포지토리 만들기

 

이 작업의 레포지토리 클래스엔 추가 로직이 필요없으므로 NewsRepository는 네트워크 데이터 소스의 프록시 역할을 한다. 이렇게 추가 추상화 계층을 추가할 때의 이점은 메모리 내 캐싱 섹션을 확인한다.

 

// NewsRepository is consumed from other layers of the hierarchy.
class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource
) {
    suspend fun fetchLatestNews(): List<ArticleHeadline> =
        newsRemoteDataSource.fetchLatestNews()
}

 

메모리 내 데이터 캐싱 구현

 

뉴스 앱에 새 요구사항이 도입됐다고 가정한다. 유저가 화면을 열면 이전에 요청이 생성된 경우 캐시된 뉴스가 사용자에게 표시돼야 한다. 그러지 않으면 앱이 최신 뉴스를 가져오기 위해 네트워크 요청을 해야 한다.

새 요구사항이 있으므로 앱은 유저가 앱을 열고 있는 동안 메모리에 최신 뉴스를 보존해야 한다. 따라서 이는 앱 지향 작업이다.

 

캐시

 

유저가 앱에 있는 동안 메모리 내 데이터 캐싱을 추가해서 데이터를 보존할 수 있다. 캐시는 유저가 앱에 있는 한 특정 시간 동안 메모리에 일부 정보를 저장하기 위해 실행된다.

캐시 구현은 여러 형태를 가질 수 있다. 간단한 변경 가능 변수부터 여러 쓰레드에서 읽기, 쓰기 작업을 금지하는 더 정교한 클래스까지 다양할 수 있다. 유스케이스에 따라 레포지토리 or 데이터 소스 클래스 안에 캐싱을 구현할 수 있다.

 

네트워크 요청 결과 캐시

 

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 }
    }
}

 

작업을 화면보다 길게 유지

 

네트워크 요청이 진행되는 동안 유저가 화면에서 벗어나면 취소되고 결과가 캐시되지 않는다. 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 안의 로직은 계속 실행된다.

 

데이터 저장 및 디스크에서 가져오기

 

북마크한 뉴스, 유저 환경설정 같은 데이터를 저장하려 한다고 가정한다. 이런 유형의 데이터는 유저가 네트워크에 연결돼 있지 않아도 프로세스가 종료된 후에도 남아 있어서 접근할 수 있어야 한다.

작업 중인 데이터가 프로세스 중단 후에도 유지돼야 한다면 아래 방법 중 하나로 데이터를 디스크에 저장해야 한다.

 

  • 쿼리해야 하거나 참조 무결성이 필요, 부분 업데이트가 필요한 대규모 데이터 세트는 Room DB에 저장한다
  • 쿼리하거나 부분 업데이트하지 않고 검색, 설정해야 하는 소규모 데이터 세트는 DataStore를 사용한다
  • JSON 객체 같은 데이터 청크는 파일을 사용한다

 

각 데이터 소스는 하나의 소스에서만 작동하며 특정 데이터 유형(News, Authors, NewsAndAuthors 등)에 대응한다.

데이터 소스를 사용하는 클래스는 데이터가 저장되는 방식(DB 또는 파일)을 알 수 없다.

 

데이터 소스로 사용되는 Room

 

각 데이터 소스는 특정 유형의 데이터에 대해 한 소스만 사용해야 하므로 Room 데이터 소스는 데이터 접근 객체(DAO) 또는 DB 자체를 매개변수로 수신할 수 있다.

추가 로직이 필요없다면 테스트에서 쉽게 대체할 수 있는 인터페이스라서 DAO를 레포지토리에 직접 삽입할 수 있다.

데이터 소스로 사용되는 DataStore

 

DataStore는 사용자 설정 같은 키밸류 쌍을 저장하기에 적합하다. 시간 형식, 알림 환경설정, 유저가 뉴스 항목을 읽은 후 표시하거나 숨길지 여부 등이 있다.

DataStore가 지원하는 데이터 소스에는 특정 유형이나 앱의 특정 부분에 해당하는 데이터가 포함돼야 한다.

읽기는 값이 업데이트될 때마다 방출되는 Flow로 노출되므로 더욱 그렇다. 따라서 관련 환경설정을 같은 DataStore에 저장해야 한다.

예를 들어 알림 관련 환경설정만 처리하는 NotificationsDataStore, 뉴스 화면 관련 환경설정만 처리하는 NewsPreferencesDataStore가 있을 수 있다. 이렇게 하면 업데이트 범위를 더 잘 지정할 수 있다. newsScreenPreferencesDataStore.data Flow가 화면과 관련된 환경설정이 바뀔 때만 발생하기 때문이다.

 

WorkManager를 써서 작업 예약

 

뉴스 앱에 다른 요구사항이 도입됐다고 가정한다. 기기가 충전되고 무제한 네트워크에 연결돼 있는 한 최신 뉴스를 정기적으로 자동으로 가져오는 옵션을 유저에게 제공해야 한다. 따라서 비즈니스 지향 작업이 된다.

이렇게 하면 유저가 앱을 열 때 기기가 연결되지 않아도 유저가 최근 뉴스를 볼 수 있다.

 

WorkManager를 쓰면 신뢰할 수 있는 비동기 작업을 예약할 수 있으며 제약 조건을 관리할 수 있다. 영구 작업에 권장되는 라이브러리다. 위에 정의된 작업을 실행하기 위해 Worker 클래스인 RefreshLatestNewsWorker가 생성된다.

이 클래스는 최신 뉴스를 가져와 대스크에 캐시하기 위해 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 또는 PaymentsTasksDataSource 같이 책임이 있는 데이터에 따라 이름이 지정된다. 특정 데이터 유형 관련 모든 작업은 같은 클래스에 캡슐화돼야 한다.

앱 시작 시 작업을 트리거해야 하는 경우 Initializer에서 레포지토리를 호출하는 앱 시작 라이브러리를 써서 WorkManager 요청을 트리거하는 게 좋다.

반응형
Comments