Android Fundamentals - Repository

Posted by RoadtoS7 on January 28, 2021 · 6 mins read

알게된 내용

:one: 오프라인 캐싱

  • 오프라인 캐시를 구현해서 네트워크가 아닌 데이터베이스에서 데이터를 가져와서 보여줄 수 있다.
    그 결과, 사용자들은 기기가 오프라인 상태일 때에도 앱을 사용할 수 있다.

  • 오프라인 캐시를 구현하기 위해서 Android에서는 Room을 사용하여 데이터를 기기의 로컬 데이터베이스에 영구적으로 저장한다.

  • 그리고 Room 데이터베이스는 레포지터리 패턴을 이용하여 접근하고 관리한다.

    • 레포지터리 패턴은 데이터 소스를 앱의 나머지 부분과 분리하는 디자인 패턴이다.
    • 이 패턴은 데이터 소스가 아닌 앱의 나머지 부분에게 데이터에 접근할 수 있는 clean API를 제공한다.


:two: 관심사 분리에 따른 객체 분리

  1. domain 객체: 앱안에 있는 것들을 나타내는 코틀린 데이터 클래스
    • 예를 들어서 앱에서 뷰로 나타나거나, 앱에서 조작되는 데이터들을 말한다.
  2. 네트워크 객체(Data Transfer Object): 네트워크 호출을 준비하거나 파싱하는 객체들
    • 일반적으로 네트워크 호출결과를 파싱하여 도메인 객체로 변환하는 함수를 갖는다.
  3. 데이터베이스 객체: 데이터베이스와 맵핑되는 객체들
  • 도메인 객체, 네트워크 객체, 데이터베이스 객체를 서로 분리하는 것이 좋다.
    이렇게 세가지 객체를 나누는 것은 관심사 분리의 원칙을 따르는 것이다.
    만약 네트워크 응답이나, 데이터베이스 스키마가 변경된다면, 전체 코드를 바꾸지 않고도 앱의 일부만 변경하고 관리할 수 있다.


:three: 캐싱

  • 네트워크로부터 데이터를 가져온 이후, 앱은 기기의 저장소에 데이터를 저장함으로써 데이터를 캐싱해놓을 수 있다.
  • 데이터를 캐시해놓은 이후, 기기가 오프라인일 때 혹은 이전에 가져온 동일한 데이터가 다시 필요할 때 다시 접근할 수 있다.

캐싱 방법

  • 안드로이드에서 캐싱을 구현하는 방법은 여러가지가 있다.
  1. Retrofit
    • Retrofit은 안드로이드에서 type-safe한 REST 클라이언트를 구현하는데 사용되는 네트워킹 라이브러리이다.

    • Retrofit이 네트워킹 결과를 로컬에 저장해놓도록 설정할 수 있다.

    • 사용할 때: 단순한 요청 혹은 응답, 자주 일어나지 않는 네트워크 호출, 작은 데이터 집합을 캐싱할 때 사용하면 좋다.

  2. SharedPreferences
    • key, value 쌍 형태의 데이터를 저장하기 위해 SharPreferences를 사용할 수 있다.

    • 사용할 때: 적은 양의 key, value 쌍이며, value가 단순한 형태일 경우 사용하면 좋다.
      (왜냐하면, SharedPreferences의 value로는 primitve data type 만 저장할 수 있기 때문이다.)
      따라서 많은 양의 구조화된 데이터를 저장할 때는 사용할 수 없다.

  3. 앱의 내부 저장소(internal storage)
    • 앱의 내부 저장소에 접근하여 데이터를 파일로 저장할 수 있다.

    • 우리 앱의 패키지 이름이 앱의 내부 저장소 디렉토리 이름을 나타낸다. 안드로이드 파일 시스템에서 우리 앱만 접근할 수 있는 아주 특별한 장소이다.

    • 또한 앱이 삭제되면, 내부 저장소도 같이 삭제된다.

    • 사용할 때: 파일 시스템이 해결해야만 할 때, 예를 들어서 미디어 파일이나 데이터파일을 저장해야 하며, 그 파일을 우리가 다뤄야만 할 때, 사용하면 좋다.
      복잡하고 구조화된 데이터를 저장할 때 이 방법을 사용할 수 없다.

  4. Room
    • 복잡하고 구조화된 데이터를 저장할 때 권장되는 방법이다.
  • 왜냐하면, 구조화된 데이터를 기기의 파일 시스템에 저장하는 가장 좋은 방법은 SQLite database에 저장하는 것이기 때문이다.


:four: Room을 이용하여 캐싱하기

  • 앱이 네트워크로부터 데이터를 가져올 때, 이 데이터를 바로 화면에 보여주는 것이 아니라, 데이터베이스에 저장한 후에, 데이터베이스로부터 데이터를 가져와서 화면에 보여준다.
  • 이렇게 했을 때 장점:
    1. 데이터가 항상 최신상태임을 보장한다.
    2. 이렇게 하면 앱이 데이터를 가지고 화면을 구현할 때까지의 시간인 앱 로딩 시간을 줄인다.
  • 아래 그림은 데이터를 캐싱했을 때 로직을 보여준다.

캐싱하여 데이터 가져오는 로직

  • DAO에서 데이터를 가져오는 메서드의 경우 타입을 LiveData로 하면 데이터베이스의 데이터가 변할 때마다, UI가 변하도록 할 수 있다.
  • **Database는 여러개의 인스턴스가 생기는 것을 막기 위해서 싱글톤 객체인 것이 좋다. **
fun getDatabase(context: Context): VideosDatabase {
   synchronized(VideosDatabase::class.java) {
       if (!::INSTANCE.isInitialized) {
           INSTANCE = Room.databaseBuilder(context.applicationContext,
                   VideosDatabase::class.java,
                   "videos").build()
       }
   }
   return INSTANCE
}
  • Database 인스턴스가 기존에 생성된 것이 있는지 확인하기 위해서 .isIntialized 코틀린 프로퍼티을 사용했다.
    • isInitialized 프로퍼티는 lateinit 프로퍼티가 값이 할당되었으면 true를 반환하고 그렇지 않으면 false를 반환한다.

:five: Repository 패턴

  • Repository 패턴은 앱을 데이터와 나머지 부분으로 분리하는 디자인 패턴을 말한다.
  • repository는 데이터 소스가 아닌 앱의 나머지 부분에 데이터에 접근할 수 있는 clean API를 제공한다.
  • Repository 패턴에서는 Repository가 데이터 소스(지속성 모델, 웹 서비스, 캐시)와 앱의 나머지를 중개한다.

  • 아래 그림은 LiveData를 사용하는 Activity와 같은 앱의 구성요소가 repository를 통해서 앱의 데이터 소스와 통신하는 방식을 보여준다.

Repository 패턴을 사용했을 때 앱의 구조도

  • Repository를 구현하기 위해서, Repository를 담당하는 클래스를 만든다.
  • Repository를 사용하는 것은 코드 분리와 아키텍처에서 권장된다.

Repository를 사용했을 때 장점

  • Repository 모듈은 데이터 작업을 관리하고 여러 백엔드를 둘 수 있도록 한다.
  • 일반적으로 실제 앱은 네트워크로부터 데이터를 가져올지 혹은 로컬 데이터베이스에 저장되어있는 데이터를 사용할지 결정하는 로직을 구현하고 있다.
  • 이렇게 하면, 코드를 모듈화하여 테스트할 수 있다. 레포지터리를 쉽게 모형화하고 나머지 코드를 테스트할 수 있다.

오프라인 캐싱에서 Repository

  • 데이터베이스는 오프라인 캐시를 다루는 로직을 가지고 있지 않다. 단지 데이터를 저장하고 불러오는 메서들만 가지고 있다.
  • Repository에서 데이터를 네트워크로부터 가져오고 데이터베이스에 데이터를 저장하는 역할을 한다.

:six: Android 내장 DB의 위치

  • Android 기기의 내부 DB는 디스크(파일시스템)에 위치합니다.
    따라서 데이터베이스에 데이터를 저장하기 위해서는 디스크 IO를 수행해야 한다.
  • 디스크 IO는 느린 작업이며, 디스크 IO가 완료될 때까지 해당 스레드는 블락 상태가 된다.
  • 따라서 디스크 IO는 디스크 디스패처(Disk Dispatcher)내에서 수행해야 한다.
  • withConext(Dispatcher.IO){...} 를 실행하면, 디스크 디스패처는 블락킹 IO작업을 공유 스레드 풀로 넘긴다.