본문 바로가기
Mobile App

[안드로이드] - Data Layer (데이터 레이어)

by Jman 2022. 7. 26.

데이터 레이어

데이터 영역은 0~ 여러개의 데이터 소스를 각각 포함할 수 있는 저장소로 구성된다.

앱에서 처리하는 다양한 유형의 데이터 별로 저장소 클래스(Respository Class)를 만들어야한다.

 

저장소 클래스(Repository Class) 작업

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

각 데이터 소스

  • 파일
  • 네트워크 소스
  • 로컬 데이터베이스

와 같은 하나의 데이터 소스만 사용해야 한다.

데이터 소스 클래스는 데이터 작업을 위해 애플리케이션과 시스템 간의 가교 역할을 한다.

 

계층 구조의 다른 레이어(UI Layer, Domain Layer)는  데이터 소스에 직접 접근해서는 안된다.

데이터 영역의 진입점(Entry Point)은 항상 저장 클래스여야 한다.

 

즉, State Holder Class, Use Case Class 에는 데이터 소스가 직접 종속 항목으로 있어서는 안된다.

저장소 클래스를 진입점으로 사용하면 아키텍처의 다양한 레이어를 독립적으로 확장할 수 있다.

 

또한 데이터 레이어는 노출된 데이터를 변경 불가능해야 한다.

그래야 값을 일관되지 않은 상태로 만들 위험이 있는 다른 클래스에 의한 조작이 불가능해진다.

 

종속 항목 삽입 권장사항에 따라, 저장소는 데이터 소스를 생성자의 종속 항목으로 사용한다.

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

 

 

API 노출

데이터 영역 클래스는 일반적으로

  • 원샷 생성
  • 조회
  • 업데이트 및 삭제(CRUD) 호출
  • 시간 경과에 따른 데이터 변경사항에 관해 알림 받는 함수 호출
  • 등..

역할을 한다.

  • 원샷 작업 : 데이터 영역에서 Kotlin 의 정지 함수를 노출해야 한다. 자바의 경우 데이터 영역에서 작업 결과 또는 RxJava Single, Maybe 또는 Completable 유형에 대한 콜백을 제공하는 함수를 노출해야 한다.
  • 시간 결과에 따른 데이터 변경사항에 관해 알림 : 데이터 영역에서 Kotlin 흐름을 노출해야한다. 자바의 경우 데이터 영역에서 새 데이터 또는 RxJava Observable 또는 Flowable 유형을 내보내는 콜백을 노출해야 한다.

 

가이드 이름 지정 규칙

저장소 클래스의 명명 규칙 : 데이터 유형 + 저장소

ex) NewsRepositoryMoviesRepository, PaymentsRepository

 

데이터 소스 클래스의 명명 규칙 : 데이터 유형 + 소스 유형 + DataSource

ex) NewsRemoteDataSource, NewsLocalDataSource, NewsNetworkDataSource, NewsDiskDataSource

 

단, 구현 세부 정보에 따라 데이터 소스의 이름을 지정하지마세요.

ex) UserSharedPreferencesDataSource

 

 

여러 수준의 저장소

더 복잡한 비즈니스 요구사항이 포함된 일부 경우에는 저장소가 다른 저장소에 종속 되어야 할 수 있다.

관련된 데이터가 여러 데이터 소스의 집계이너간, 책임이 다른 저장소 클래스에 캡슐화 되어야 하기 때문이다.

또한, 일반적으로 일부 개발자는 다른 저장소 클래스 관리자에 종속된 저장소 클래스, 예를 들어 UserRepository 대신에 UseManager 를 호출했다. 원하는 경우 이 이름 지정 규칙을 사용할 수가 있다.

 

 

정보 소스

각 저장소(Repo) 가 하나의 정보 소스를 정의하는 것이 중요하다.

정보 소스는 항상 일관되고 정확하며 최신 상태인 데이터를 포함하는 걸 알 것이다.

오프라인 우선 지원을 제공하려면 데이터베이스와 같은 로컬 데이터소스를 정보 소스로 사용하는 것이 좋다.

 

 

수명 주기

데이터 영역의 클래스 인스턴스는 가비지 컬렉션 루트에서 연결할 수 있는 한 메모리에 남아 있다.

이는 대개 애플리 케이션의 다른 객체에 참조된다.

 

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

이를 클래스 인스턴스의 수명 주기라고도 한다.

 

또한, 클래스의 책임이 전체 애플리케이션에 중요할 경우 해당 클래스의 인스턴스 범위를 Application 클래스로 지정할 수 있다.

이렇게 되면 해당 클래스 인스턴스가 애플리케이션의 수명 주기를 따르게 된다.

 

또는 앱의 특정 흐름에서(ex. 등록, 로그인흐름)만 동일한 인스턴스를 재사용해야 하는 경우, 흐름의 수명주기를 소유한 클래스인스턴스 범위지정해야 한다.

 

데이터 작업 유형

  • UI 지향 작업 : 사용자가 특정 화면에 있을 때만 관련이 있고 사용자가 화면에서 멀어지면 취소 된다. 예를 들어 데이터베이스에서 얻은 일부 데이터를 표시한다.
  • 앱 지향 작업 : 앱이 열려 있는 한 관련이 있다. 앱이 닫히거나 프로세스가 종료되면 이러한 작업도 취소된다. 예를 들어 네트워크 요청의 결과를 필요에 따라 나중에 사용할 수 있도록 캐시하는 경우
  • 비즈니스 지향 작업 : 이 작업은 작업 도중 취소할 수 없다. 프로세스 종료 후에도 유지된다. 예를 들어 사용자가 프로필에 게시하고 싶은 사진 업로드를 완료하는 작업

데이터 영역을 사용하고 설계하는 방법

예시 가이드를 뉴스 앱으로 설명한다.

 

네트워크 요청

네트워크 요청은 Android 앱에서 실행할 수 있는 가장 일반적인 작업 중 하나다.

 

뉴스 앱은 네트워크에서 가져온 최신 뉴스를 사용자에게 표시해야 한다.

따라서 앱에는 네트워크 작업을 관리하기 위한 데이터 소스 클래스(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>
}

 

저장소 만들기

NewsRepository 는 네트워크 데이터 소스의 프록시 역할을 한다.

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

 

 

메모리 내 데이터 캐싱 구현

사용자가 화면을 열면 이전에 요청이 생성된 경우, 캐시된 뉴스가 사용자에게 표시되어야 하며,

그러지 않았을 경우 앱이 최신 뉴스를 가져오기 위해 네트워크 요청을 해야할 때가 있을 것이다.

 

이럴 경우, 앱은 사용자가 앱을 열고 있는 동안 메모리에 최신 뉴스를 보존해야 하며, 

이를 앱 지향 작업이라 한다.

 

캐시 

사용자가 앱에 있는 동안 메모리 데이터 캐싱을 추가하여 데이터를 보존할 수가 있다.

캐시는 사용자가 앱에 있는 한 특정 시간 동안 메모리에 일부 정보를 저장하기 위해 실행된다.

 

캐시 구현은 다양한 형태를 취할 수가 있다.

  • 간단한 변경 가능 변수,
  • 여러 스레드에서 읽기/쓰기 작업을 금지하는 더욱 정교한 클래스

등. 다양하다. 사용 사례에 따라 저장소 또는 데이터 소스 클래스 내에 캐싱을 구현할 수 있다.

 

네트워크 요청 결과 캐시

 

아래 예시 코드를 통해 설명을 해보면,

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

변경 가능한 변수를 사용하여 최신 뉴스를 캐시하는 코드다.

  1. 여러 스레드에서 읽기 및 쓰기를 금지하기 위해 Mutex를 사용
  2. Mutex 로 쓰기가 금지된 저장소의 변수에 최신 뉴스 정보를 캐시한다.
  3. 네트워크 요청 결과가 성공하면 데이터가 lastestNews 변수에 할당된다.

 

작업을 화면보다 길게 유지

 

네트워크 요청이 진행되는 동안 사용자가 화면에서 벗어나면 취소되고, 결과가 캐시되지 않는다.

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는 외부 범위에서 코루틴을 시작하는 데 사용된다.

  1. 네트워크 요청이 다시 발생하고 결과가 캐시에 저장될 때까지 정지하기 위해 await가 새 코루틴에서 호출.
  2. 그때 사용자가 여전히 화면에 있다면 최신 뉴스가 표시.
  3. 사용자가 화면에서 벗어나면 await가 취소되지만 async 내부의 로직은 계속 실행

 

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

북마크한 뉴스와 사용자 환결설정과 같은 데이터를 저정하려 한다고 가정해보겠다.

이러한 유형의 데이터는 사용자가 네트워크에 연결되어 있지 않더라도 프로세스가 종료된 후에도 남아 있어 엑세스할 수 있어야 한다.

 

작업 중인 데이터가 프로세스 중단 후에도 유지되어야 하는 경우,

  • 쿼리해야 하거나, 참조 무결성이 필요하거나 부분 업데이트가 필요한 대규모 데이터일 경우 Room 데이터베이스에 데이터 저장.
  • 쿼리하거나, 부분적으로 업데이트하지 않고, 검색 및 설정해야 하는 소규모 데이터일 경우에 DataStore를 사용
  • JSON 객체와 같은 데이터 청크의 경우 파일을 사용.

각 데이터 소스는 하나의 소스에서만 작동하고, 특정 데이터 유형에 대응한다.

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

 

* 쿼리 : 웹 서버에 특정한 정보를 보여달라는 웹 클라이언트 요청(주로 문자열을 기반으로 한 요청이다)에 의한 처리이다. 

* 참조 무결성 : 기본 키와 참조 키 간의 관계가 항상 유지됨을 보장

 

 

 

 

참고

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

 

데이터 영역  |  Android 개발자  |  Android Developers

데이터 영역 UI 레이어에는 UI 관련 상태 및 UI 로직이 포함되지만 데이터 영역에는 애플리케이션 데이터 및 비즈니스 로직이 포함됩니다. 비즈니스 로직은 앱에 가치를 부여하는 요소로, 애플리

developer.android.com