[안드로이드 코드랩] Android Kotlin Fundamentals: Repository

홍석규·2022년 2월 24일
0

본 포스트는 안드로이드 기초 코드랩을 학습하고 정리한 내용입니다.
https://developer.android.com/codelabs/kotlin-android-training-repository?index=..%2F..android-kotlin-fundamentals&hl=ko#0

Repository

이번에 만들어볼 앱은 Google Android developer relations team 에서 만든 튜토리얼 영상을 리스트 형식으로 보여주는 앱이다. 앱은 Retrofit을 이용해 video url list를 서버로부터 받아온다. 그리고 리싸이클러뷰를 이용해서 화면에 뿌려주는데 앱은 뷰모델과 라이브데이터를 이용해서 데이터를 hold해두고 ui상에 display해준다.

  • 중요한건 DevBytes 앱은 online-only 라는 것이다. 그래서 사용자는 인터넷에 연결이 되어있어야하는데 이번 코드랩에선 네트워크가 아닌 로컬 디비를 이용해서 데이터를 오프라인 캐싱하는 기능을 배울것이다.

Caching의 개념

앱이 서버로부터 데이터를 가져오면 앱은 디바이스 저장소에 데이터를 저장할 수 있다. 이를 캐싱이라고 하는데 캐싱을 하는 이유는 디바이스가 offline 모드로 전환 되었을 때, 같은 데이터에 다시 접근하고 싶을 때 캐싱된 데이터를 사용할 수 있기 때문이다.

Caching 적용 예시

  • 레트로핏은 type-safe한 rest api호출을 지원해주는 안드로이드 라이브러리다. 모든 네트워크 호출 결과를 로컬에 저장 해둘 수 있다.
    • 간단한 request와 response인 경우, 빈번하지 않은 네트워크 호출과 데이터셋이 작은경우 적합한 방법이다.
  • key:value 쌍으로 제공되는 SharedPreference를 이용할 수 있다.
    • 적은 수의 키와 간단한 값에 적합한 솔루션이다. 거대한 양의 구조화된 데이터를 저장하기에는 적합하지 않다.
  • 앱의 내부 저장소에 접근해서 데이터 파일을 저장하는 방법을 사용할 수 있다. 앱 패키지 이름은 안드로이드 파일 시스템 내에 특정한 위치에 있는 내부 저장 디렉터리를 지정한다. 파일 시스템 내의 디렉토리는 앱마다 private하게 유지되며, 앱이 삭제되면 자동으로 지워진다.
    • 파일 시스템을 해결할 수 있는 요구사항이 있으면 좋은 선택지가 된다, 만약에 미디어 파일이나 데이터 파일을 저장해야하고 해당 파일들을 직접 관리 해야한다면 이 방법을 사용하는것이 좋다. 역시 복잡하고 구조화된 데이터를 저장하기엔 적합하지 않다.
  • 마지막 방법은 Room을 이용하는 것이다. Room은 SQLite를 추상화한 SQLite와 매핑해줄 수 있게 지원해주는 라이브러리다.
    • 복잡하고 구조화된 데이터를 저장할 때 가장 추천되는 방법이다. 디바이스 파일 시스템에 구조화된 데이터를 저장하는 최고의 방법은 local sqlite database를 이용하는것이다.

로컬 디비에 저장하는것도, 간단한 데이터의 경우 프리퍼런스를 이용해서 저장하는것도 모두 캐싱에 해당되는 이야기다.

가장 중요한 컨셉은 앱이 실행될 때마다 매번 네트워크에 요청을 시도하지 말라는 것이다. 대신 디비에서 데이터를 가져와서 화면에 뿌려주는것이 좋다. 이렇게 하는게 앱 로딩시간을 줄여 줄 수 있다.

네트워크를 이용해 데이터를 가져올 때 화면에 바로 뿌려주지 말고 우선 데이터베이스에 저장해두자.

api 호출 → 로컬 데이터베이스 저장 → 로컬 데이터베이스 접근 후 데이터 화면에 표시 이 흐름을 사용하자. 그래야 오프라인 모드에서도 항상 데이터를 가져올 수 있기 때문이다. 앱이 오프라인 상태가 되어도 여전히 로컬 캐시데이터를 가져올 수 있다.

Repository 패턴

  • repository 패턴은 데이터 소스를 앱의 다른 부분과 분리 시키는 디자인 패턴이다. repository는 데이터 소스 영역과 (persistent model, web service, caches)과 앱의 나머지 영역을 이어주는 역할을 한다.
  • 레포지토리를 구현하기 위해 VideosRepository와 같은 레포지토리 클래스가 필요하다. 레포지토리 클래스는 앱의 영역에서 데이터 소스영역을 분리 시키고 앱에 data에 접근할 수 있는 Clean API를 제공해줄 수 있다. 레포지토리 클래스를 사용하는것이 코드를 분리시키고 아키텍처 측면에서 가장 추천하는 방법이다.

Repository 패턴 사용의 이점

레포지토리 모듈에서는 데이터 작업을 처리하고 여러 백엔드를 사용할 수 있게 해준다. 실제로 코드랩이 아닌 앱을 구현하게 되면 데이터를 네트워크에서 가져 올 것인지 ? 아니면 로컬 디비에 캐싱한 데이터로 부터 가져올 것인지 에 대한 로직을 구현하게 된다. 레포지토리 패턴은 이런 경우 코드를 모듈화 하고 테스트 가능하게 도와준다. repository를 쉽게 mock up하고 나머지 코드를 테스트 할 수 있게된다.

레포지토리 생성

이전 챕터에서 룸 디비를 생성하고 crud하는 쿼리를 dao 형태로 생성해두었는데 Room db는 `오프라인 캐시를 관리할 로직을 가지고 있지 않다. 오직 data에 접근하고 가져오는 메서드만 있을뿐` 레포지토리는 네트워크에서 데이터를 가져오고 database를 최신 상태로 유지하는 로직을 구현할 수 있다.

안드로이드의 데이터베이스는 파일 시스템이나, 디스크에 저장하기 위해 disk I/O를 반드시 수행하게 된다. disk로부터 데이터를 읽고 쓰는 disk I/O 작업은 느리고 항상 I/O 작업이 끝나기 전 까지 해당 쓰레드를 블락 시켜버린다. 이러한 이유로 disk I/O는 코루틴의 I/O dispatcher를 이용해 실행시켜야 한다. I/O dispatcher는 블락되는 I/O 작업을 공유 쓰레드 풀을 이용해서 처리할 수 있도록 설계 되어있다.`

먼저 레포지토리 클래스를 만들자.

class VideosRepository(private val database: VideosDatabase) {

suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
       Timber.d("refresh videos is called");
       val playlist = DevByteNetwork.devbytes.getPlaylist()
       database.videoDao.insertAll(playlist.asDatabaseModel())
   }
}

}
  • 레포지토리 클래스는 내부에 VideosDatabase를 프로퍼티로 가진다. 생성자를 통해 외부에서 주입 해주게 구현 해두었으며 필요에 따라 api 호출 로직을 수행하는 레트로핏 프로퍼티를 추가해줄 수 있다
  • suspend 메서드인 refreshVidoes 메서드를 추가해준다. suspend로 선언한 이유는 viewModelScope의 코루틴에서 사용할 수 있게 만든것이며 내부에는 withContext(Dispatchers.IO)로 감싼 새로운 코루틴을 생성한다.
    • 위에서도 언급 했듯이 IO로직을 main Thread에서 수행할 수 없기 때문에 withContext를 이용해 Dispathcers.IO에게 로직 수행을 넘겨준다. 내부에는 retrofit을 이용해 서버에서 데이터를 받고 database에 insert 하는 로직을 수행한다.
    • getPlayList()메서드 역시 suspend 메서드라서 마치 동기식으로 동작 하는것처럼 코드 가독성을 높일 수 있다.

그리고 디비에서 데이터를 받아오는 로직을 추가해준다.

val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) {
   it.asDomainModel()
}
  • 위 코드에서 유심히 살펴봐야 할 부분이 있는데 Transformations.map()과 database.videoDao.getVideos() 메서드다.
  • 먼저 Transformations.map()은 첫번째 인자로 받은 라이브데이터를 두번째 인자로 받는 mapFunction()을 이용해 매핑해서 새로운 라이브데이터를 반환 해준다.

database.videoDao.getVidoes() 메서드를 살펴보면

@Dao
interface VideoDao{
    @Query("select * from databasevideo")
		fun getVideos(): LiveData<List<DatabaseVideo>>
}
  • dao 메서드인데 LiveData를 반환하고 있다. room이 liveData를 반환 할 경우 디비에서 데이터 갱신이 발생하는 경우 자동으로 해당 메서드가 트리거 된다. 즉 다음과 같은 실행 흐름을 보이게 된다.
    1. refreshVideos() 호출

    2. database.videoDao.insertAll() 호출

    3. 데이터베이스에 데이터를 새로 넣게 될 경우 기존 db에서 업데이트가 발생한것이므로 getVideos()자동으로 호출 (즉 라이브데이터 갱신)

    4. Transformations.map() 메서드 역시 첫번째 인자로 받는 라이브데이터가 갱신 될 경우 자동으로 트리거 되기 때문에 vidoes 데이터 갱신

      이런 흐름을 가질 수 있게된다.

뷰모델에서 레포지토리 사용하기

기존에 만들어 둔 DevByteViewModel에서 레포지토리를 생성하자. 우선 멤버변수로 Repository를 추가해주자.

/**
* The data source this ViewModel will fetch results from.
*/
private val videosRepository = VideosRepository(getDatabase(application))
  • 실제로 앱을 개발 할 때는 이렇게 구현하는것이 좋지않다. Repository를 외부에서 주입할 수 있게 해주고, 구현체가 아닌 역할인 인터페이스로 추상화 시켜 결합도를 낮춘 다음 DI 패턴을 이용해서 구현하는것이 좋다. 그래야 테스트 하기도 수월하고 수정시 사이드이펙이 발생하지 않기 때문이다.

이제 Repository에서 데이터를 가져오는 로직을 추가하자

private fun refreshDataFromRepository() {
   viewModelScope.launch {
       try {
           videosRepository.refreshVideos()
           _eventNetworkError.value = false
           _isNetworkErrorShown.value = false

       } catch (networkError: IOException) {
           // Show a Toast error message and hide the progress bar.
           if(playlist.value.isNullOrEmpty())
               _eventNetworkError.value = true
       }
   }
}
  • 먼저 뷰모델 스코프를 이용해 코루틴을 생성한 다음 videosRepository.refreshVideos()를 호출해준다. 역시 코루틴으로 구현 했기 때문에 쓰레드를 신경 쓸 필요 없이 동기처럼 코드를 작성해줄 수 있다. 만약 refreshVidoes()메서드에서 에러가 발생해서 IOException을 던진다면 catch 구문에 걸려서 catch로직이 수행 될 것이다.
  • 위 로직이 수행되면 repository내에서 서버에서 데이터를 받아서 로컬 디비에 저장 하게 되는데 아까 위에서 설명했던 repository.videos live data도 자동으로 갱신되게 된다. 해당 데이터를 viewModel에서 참조할 수 있게 해주자
    //viewModel 코드
    val playlist = videosRepository.videos
    
    // Fragment 코드
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            viewModel.playlist.observe(viewLifecycleOwner, Observer<List<DevByteVideo>> { videos ->
                videos?.apply {
                    viewModelAdapter?.videos = videos
                }
            })
        }
    • 자동으로 vidoesRepository.vidoes가 갱신 된다는 것을 알게 되었고 Fragment에 추가해 둔 옵저버에서 onChanged()메서드가 트리거 될 것이다. viewModelAdapter의 데이터를 갱신 해주는 로직이다.

즉 정리하자면 ViewModel 생성 → repository.refreshVideos() 호출 → 서버에서 데이터 fetch, 로컬 db 저장, 자동으로 갱신된 데이터 fetch → liveData observer onChanged() 콜백 메서드 트리거 → recyclerView data 갱신 및 notifyDataSetChanged() 호출 → 화면에 표시

이렇게 동작 하게 된다.

코드랩에서는 어떻게 데이터를 로컬 디비에 캐싱해야 하는지 방법과 repository 패턴에 대해서 설명 해주고있다. 지금 코드는 viewModel이 init()될 때마다 위 로직이 트리거 되는데 실제 앱을 구현하게 될 경우 더 복잡하고 다양한 데이터 refresh 방법을 구현 해야할 것이다.

profile
학습한 내용을 공유하고 기록합니다.

0개의 댓글