1. 시작하기 전에
소개
이 Codelab에서는 오프라인 캐싱을 사용하여 앱의 사용자 환경을 개선합니다. 많은 앱이 네트워크의 데이터를 사용합니다. 앱이 실행될 때마다 서버에서 데이터를 가져오고 사용자에게 로드 화면이 표시되면 사용자 환경에 부정적인 영향을 미쳐 사용자가 앱을 제거할 수 있습니다.
사용자는 앱을 실행할 때 앱에 데이터가 빠르게 표시되기를 기대합니다. 오프라인 캐싱을 구현하면 이 목표를 달성할 수 있습니다. 오프라인 캐싱은 앱이 네트워크에서 가져온 데이터를 기기의 로컬 저장소에 저장한다는 의미로, 액세스 속도가 더 빨라집니다.
앱이 네트워크에서 데이터를 가져올 수 있을 뿐만 아니라 이전에 다운로드한 결과의 오프라인 캐시를 유지할 수 있으므로 앱이 이러한 여러 데이터 소스를 구성할 방법이 필요합니다. 앱 데이터의 단일 정보 소스 역할을 하고 데이터 소스(네트워크, 캐시 등)를 뷰 모델에서 분리하는 저장소 클래스를 구현하면 됩니다.
기본 요건
다음을 잘 알고 있어야 합니다.
- 데이터 지속성 라이브러리 Room
- Retrofit 네트워킹 라이브러리 사용
- 기본 Android 아키텍처 구성요소
ViewModel
,ViewModelFactory
,LiveData
- LiveData 클래스의 변환
- 코루틴 빌드 및 실행
- 데이터 결합의 결합 어댑터
학습할 내용
- 저장소를 구현하여 앱의 나머지 부분에서 앱의 데이터 레이어를 분리하는 방법
- 캐시된 데이터를 저장소를 사용하여 로드하는 방법
실행할 작업
- 저장소를 사용하여 데이터 레이어를 분리하고 저장소 클래스를
ViewModel
과 통합합니다. - 오프라인 캐시의 데이터를 표시합니다.
2. 시작 코드
프로젝트 코드 다운로드
폴더 이름은 RepositoryPattern-Starter
입니다. Android 스튜디오에서 프로젝트를 열 때 이 폴더를 선택하세요.
이 Codelab의 코드를 가져와서 Android 스튜디오에서 열려면 다음을 실행합니다.
코드 가져오기
- 제공된 URL을 클릭합니다. 브라우저에서 프로젝트의 GitHub 페이지가 열립니다.
- 브랜치 이름이 Codelab에 지정된 브랜치 이름과 일치하는지 검토하고 확인합니다. 예를 들어 다음 스크린샷에서 브랜치 이름은 main입니다.
- 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 팝업을 엽니다.
- 팝업에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
- 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
- ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.
Android 스튜디오에서 프로젝트 열기
- Android 스튜디오를 시작합니다.
- Welcome to Android Studio 창에서 Open을 클릭합니다.
참고: Android 스튜디오가 이미 열려 있는 경우 File > Open 메뉴 옵션을 대신 선택합니다.
- 파일 브라우저에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
- 프로젝트 폴더를 더블클릭합니다.
- Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
- Run 버튼 을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.
3. 시작 앱 개요
DevBytes 앱은 Android 개발자 YouTube 채널의 DevBytes 동영상 목록을 recycler 뷰에 표시합니다. 여기서 사용자는 링크를 클릭하여 동영상을 열 수 있습니다.
시작 코드는 완전히 작동하지만 사용자 환경에 부정적인 영향을 줄 수 있는 심각한 문제가 있습니다. 사용자의 인터넷 연결이 불안정하거나 인터넷에 전혀 연결되지 않은 경우 동영상이 표시되지 않습니다. 앱이 이전에 열린 경우에도 마찬가지입니다. 사용자가 이번에는 인터넷 없이 앱을 종료한 후 다시 실행하면 앱이 동영상 목록을 다시 다운로드하려고 하지만 실패합니다.
에뮬레이터에서 이를 실제로 확인할 수 있습니다.
- Android Emulator에서 일시적으로 비행기 모드를 사용 설정합니다(Settings App > Network & Internet > Airplane mode).
- DevBytes 앱을 실행하고 화면이 비어 있는지 확인합니다.
- 나머지 Codelab을 진행하기 전에 비행기 모드를 사용 중지해야 합니다.
이는 DevBytes 앱이 데이터를 처음 다운로드한 후 나중에 사용할 수 있도록 캐시된 것이 아무것도 없기 때문입니다. 현재 앱에는 Room 데이터베이스가 포함되어 있습니다. 이를 사용하여 캐싱 기능을 구현하고 저장소를 사용하도록 뷰 모델을 업데이트하는 작업을 해야 합니다. 저장소는 새 데이터를 다운로드하거나 Room 데이터베이스에서 가져옵니다. 저장소 클래스는 뷰 모델에서 이 로직을 분리하므로 코드가 체계적이고 분리된 상태로 유지됩니다.
시작 프로젝트는 여러 패키지로 구성됩니다.
코드를 잘 알고 있는 것이 좋지만 repository/VideosRepository.kt와 viewmodels/DevByteViewModel 두 파일만 사용합니다. 먼저 캐싱을 위한 저장소 패턴을 구현하는 VideosRepository
클래스를 만들고(자세한 내용은 다음 페이지 참고) 새로운 VideosRepository
클래스를 사용하도록 DevByteViewModel
을 업데이트합니다.
그러나 코드를 바로 시작하기 전에 잠시 캐싱과 저장소 패턴에 관해 자세히 알아보겠습니다.
4. 캐싱 및 저장소 패턴
저장소
저장소 패턴은 데이터 레이어를 앱의 나머지 부분에서 분리하는 디자인 패턴입니다. 데이터 레이어는 UI와는 별도로 앱의 데이터와 비즈니스 로직을 처리하는 앱 부분을 나타내며 앱의 나머지 부분에서 이 데이터에 액세스할 수 있도록 일관된 API를 노출합니다. UI가 사용자에게 정보를 제공하는 동안 데이터 레이어에는 네트워킹 코드, Room 데이터베이스, 오류 처리, 데이터를 읽거나 조작하는 코드 등이 포함됩니다.
저장소는 데이터 소스(예: 영구 모델, 웹 서비스, 캐시) 간의 충돌을 해결하고 이 데이터의 변경사항을 중앙 집중화할 수 있습니다. 아래 다이어그램은 활동과 같은 앱 구성요소가 저장소를 통해 데이터 소스와 상호작용하는 방법을 보여줍니다.
저장소를 구현하려면 다음 작업에서 만드는 VideosRepository
클래스와 같은 별도의 클래스를 사용합니다. 저장소 클래스는 앱의 나머지 부분에서 데이터 소스를 분리하고 앱의 나머지 부분의 데이터 액세스를 위한 깔끔한 API를 제공합니다. 저장소 클래스를 사용하면 이 코드가 ViewModel
클래스와 분리되며 코드 분리와 아키텍처에 저장소 클래스를 사용하는 것이 좋습니다.
저장소 사용의 이점
저장소 모듈은 데이터 작업을 처리하고 여러 백엔드 사용을 허용합니다. 일반적인 실제 앱에서 저장소는 네트워크에서 데이터를 가져올지 아니면 로컬 데이터베이스에 캐시된 결과를 사용할지 결정하는 로직을 구현합니다. 저장소를 사용하면 뷰 모델과 같은 호출 코드에 영향을 주지 않고 다른 지속성 라이브러리로의 이전과 같은 구현 세부정보를 교체할 수 있습니다. 이는 코드를 모듈식으로, 테스트 가능하게 만드는 데도 도움이 됩니다. 쉽게 저장소를 모의 처리하고 코드의 나머지 부분을 테스트할 수 있습니다.
저장소는 앱 데이터의 특정 부분에 관한 단일 정보 소스 역할을 해야 합니다. 네트워크 리소스와 오프라인 캐시 등 여러 데이터 소스로 작업할 때 저장소는 앱의 데이터가 최대한 정확하고 최신 상태로 유지되도록 하므로 앱이 오프라인 상태일 때도 최상의 환경을 제공합니다.
캐싱
캐시란 앱에서 사용하는 데이터 저장소를 나타냅니다. 예를 들어 사용자의 인터넷 연결이 끊기는 경우 네트워크의 데이터를 일시적으로 저장할 수 있습니다. 네트워크를 더 이상 사용할 수 없더라도 앱은 여전히 캐시된 데이터로 대체할 수 있습니다. 캐시는 더 이상 화면에 표시되지 않는 활동의 임시 데이터를 저장하거나 앱 실행 사이에 데이터를 유지하는 데도 유용할 수 있습니다.
캐시는 특정 작업에 따라 여러 형태일 수 있으며 간단하거나 복잡할 수 있습니다. 다음 표는 Android에서 네트워크 캐싱을 구현하는 여러 가지 방법을 보여줍니다.
캐싱 기법 | 사용 |
Retrofit은 Android용 유형 안전 REST 클라이언트를 구현하는 데 사용되는 네트워킹 라이브러리입니다. 모든 네트워크 결과의 사본을 로컬에 저장하도록 Retrofit을 구성할 수 있습니다. | 간단한 요청 및 응답이나 드문 네트워크 호출, 소규모 데이터 세트에 적합한 솔루션입니다. |
| 소수의 키와 간단한 값(예: 앱 설정)에 적합한 솔루션입니다. 이 기법은 대량의 구조화된 데이터를 저장하는 데는 사용할 수 없습니다. |
앱의 내부 저장소 디렉터리에 액세스하여 데이터 파일을 저장할 수 있습니다. 앱의 패키지 이름은 Android 파일 시스템의 특별한 위치에 있는 앱의 내부 저장소 디렉터리를 지정합니다. 이 디렉터리는 앱에서만 사용할 수 있으며 앱이 제거될 때 삭제됩니다. | 파일 시스템에서 해결할 수 있는 특정 요구사항이 있는 경우(예: 미디어 파일이나 데이터 파일을 저장해야 하고 파일을 직접 관리해야 하는 경우) 적합한 솔루션입니다. 이 기법은 앱에서 쿼리해야 하는 복잡하고 구조화된 데이터를 저장하는 데는 사용할 수 없습니다. |
SQLite를 통해 추상화 레이어를 제공하는 SQLite 객체 매핑 라이브러리인 Room을 사용하여 데이터를 캐시할 수 있습니다. | 복잡하고 쿼리 가능한 구조화된 데이터에 권장되는 솔루션입니다. 기기의 파일 시스템에 구조화된 데이터를 저장하는 가장 좋은 방법은 로컬 SQLite 데이터베이스에 저장하는 것이기 때문입니다. |
이 Codelab에서는 Room을 사용합니다. 기기 파일 시스템에 구조화된 데이터를 저장하는 데 권장되는 방법이기 때문입니다. DevBytes 앱은 이미 Room을 사용하도록 구성되어 있습니다. 저장소 패턴을 사용해 오프라인 캐싱을 구현하여 데이터 레이어를 UI 코드에서 분리하면 됩니다.
5. VideoRepository 구현
작업: 저장소 만들기
이 작업에서는 이전 작업에서 구현한 오프라인 캐시를 관리할 저장소를 만듭니다. Room 데이터베이스에는 오프라인 캐시를 관리하는 로직이 없고 데이터를 삽입하고 업데이트하고 삭제하고 검색하는 메서드만 있습니다. 저장소에는 네트워크 결과를 가져오고 데이터베이스를 최신 상태로 유지하는 로직이 있습니다.
1단계: 저장소 추가
- repository/VideosRepository.kt에서
VideosRepository
클래스를 만듭니다.VideosDatabase
객체를 클래스의 생성자 매개변수로 전달하여 DAO 메서드에 액세스합니다.
class VideosRepository(private val database: VideosDatabase) {
}
VideosRepository
클래스 내에서 인수가 없고 아무것도 반환하지 않는refreshVideos()
라는suspend
메서드를 추가합니다. 이 메서드는 오프라인 캐시를 새로고침하는 데 사용되는 API입니다.
suspend fun refreshVideos() {
}
refreshVideos()
메서드 내에서 코루틴 컨텍스트를Dispatchers.IO
로 전환하여 네트워크 및 데이터베이스 작업을 실행합니다.
suspend fun refreshVideos() {
withContext(Dispatchers.IO) {
}
}
withContext
블록 내에서 Retrofit 서비스 인스턴스DevByteNetwork
를 사용하여DevByte
동영상 재생목록을 네트워크에서 가져옵니다.
val playlist = DevByteNetwork.devbytes.getPlaylist()
refreshVideos()
메서드 내에서 네트워크에서 재생목록을 가져온 후 재생목록을 Room 데이터베이스에 저장합니다. 재생목록을 저장하려면VideosDatabase
클래스를 사용합니다.insertAll()
DAO 메서드를 호출하여 네트워크에서 검색된 재생목록을 전달합니다.asDatabaseModel()
확장 함수를 사용하여 재생목록을 데이터베이스 객체에 매핑합니다.
database.videoDao.insertAll(playlist.asDatabaseModel())
- 다음은 호출될 때 추적할 로그 구문이 있는 전체
refreshVideos()
메서드입니다.
suspend fun refreshVideos() {
withContext(Dispatchers.IO) {
val playlist = DevByteNetwork.devbytes.getPlaylist()
database.videoDao.insertAll(playlist.asDatabaseModel())
}
}
2단계: 데이터베이스에서 데이터 검색
이 단계에서는 LiveData
객체를 만들어 데이터베이스에서 동영상 재생목록을 읽습니다. 이 LiveData
객체는 데이터베이스가 업데이트될 때 자동으로 업데이트됩니다. 연결된 프래그먼트 또는 활동이 새 값으로 새로고침됩니다.
VideosRepository
클래스에서videos
라는LiveData
객체를 선언하여DevByteVideo
객체 목록을 유지합니다.database.videoDao
를 사용하여videos
객체를 초기화합니다.getVideos()
DAO 메서드를 호출합니다.getVideos()
메서드는 데이터베이스 객체 목록을 반환하고DevByteVideo
객체 목록을 반환하지 않으므로 Android 스튜디오에서 'type mismatch' 오류가 발생합니다.
val videos: LiveData<List<DevByteVideo>> = database.videoDao.getVideos()
- 이 오류를 해결하려면
Transformations.map
을 사용하여asDomainModel()
변환 함수를 통해 데이터베이스 객체 목록을 도메인 객체 목록으로 변환합니다.
val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) {
it.asDomainModel()
}
이제 앱 저장소를 구현했습니다. 다음 작업에서는 간단한 새로고침 전략을 사용하여 로컬 데이터베이스를 최신 상태로 유지합니다.
6. DevByteViewModel에서 VideosRepository 사용
작업: 새로고침 전략을 사용하여 저장소 통합
이 작업에서는 간단한 새로고침 전략을 사용하여 저장소를 ViewModel
과 통합합니다. 네트워크에서 직접 가져오지 않고 Room 데이터베이스의 동영상 재생목록을 표시합니다.
데이터베이스 새로고침은 네트워크의 데이터와 동기화를 유지하기 위해 로컬 데이터베이스를 업데이트하거나 새로고침하는 프로세스입니다. 이 샘플 앱에서는 간단한 새로고침 전략을 사용하며 이 전략에서는 저장소의 데이터를 요청하는 모듈이 로컬 데이터를 새로고침합니다.
실제 앱에서는 전략이 더 복잡할 수 있습니다. 예를 들어 코드가 자동으로 백그라운드에서 데이터를 새로고침하거나(대역폭을 고려함) 사용자가 다음에 사용할 가능성이 가장 높은 데이터를 캐시할 수 있습니다.
- viewmodels/DevByteViewModel.kt의
DevByteViewModel
클래스 내에서VideosRepository
유형의videosRepository
라는 비공개 멤버 변수를 만듭니다. 싱글톤VideosDatabase
객체를 전달하여 변수를 인스턴스화합니다.
private val videosRepository = VideosRepository(getDatabase(application))
DevByteViewModel
클래스에서refreshDataFromNetwork()
메서드를refreshDataFromRepository()
메서드로 바꿉니다. 이전 메서드refreshDataFromNetwork()
는 Retrofit 라이브러리를 사용하여 네트워크에서 동영상 재생목록을 가져왔습니다. 새 메서드는 저장소에서 동영상 재생목록을 로드합니다. 저장소는 재생목록을 가져오는 소스(예: 네트워크, 데이터베이스 등)를 결정하여 구현 세부정보를 뷰 모델에서 제외합니다. 저장소를 사용하면 코드를 더 쉽게 유지할 수도 있습니다. 향후 데이터를 가져오기 위한 구현을 변경하는 경우 뷰 모델을 수정하지 않아도 됩니다.
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
}
}
}
DevByteViewModel
클래스의init
블록 내에서 함수 호출을refreshDataFromNetwork()
에서refreshDataFromRepository()
로 변경합니다. 이 코드는 네트워크에서 직접 가져오는 것이 아니라 저장소에서 동영상 재생목록을 가져옵니다.
init {
refreshDataFromRepository()
}
DevByteViewModel
클래스에서_playlist
속성과 그 지원 속성playlist
를 삭제합니다.
삭제할 코드
private val _playlist = MutableLiveData<List<Video>>()
...
val playlist: LiveData<List<Video>>
get() = _playlist
DevByteViewModel
클래스에서videosRepository
객체를 인스턴스화한 후 저장소의LiveData
동영상 목록을 보관할playlist
라는 새val
을 추가합니다.
val playlist = videosRepository.videos
- 앱을 실행합니다. 앱은 이전과 동일하게 실행되지만 이제
DevBytes
재생목록을 네트워크에서 가져와 Room 데이터베이스에 저장합니다. 재생목록은 네트워크에서 직접 표시되는 것이 아니라 Room 데이터베이스에서 화면에 표시됩니다.
- 차이를 확인하려면 에뮬레이터나 기기에서 비행기 모드를 사용 설정합니다.
- 앱을 다시 한번 실행합니다. '네트워크 오류' 토스트 메시지가 표시되지 않습니다. 대신 재생목록을 오프라인 캐시에서 가져와 표시합니다.
- 에뮬레이터나 기기에서 비행기 모드를 사용 중지합니다.
- 앱을 닫았다가 다시 엽니다. 앱은 네트워크 요청이 백그라운드에서 실행되는 동안 오프라인 캐시에서 재생목록을 로드합니다.
새 데이터가 네트워크에서 수신되면 화면이 자동으로 업데이트되어 새 데이터가 표시됩니다. 그러나 DevBytes
서버는 콘텐츠를 새로고침하지 않으므로 데이터 업데이트가 표시되지 않습니다.
수고하셨습니다. 이 Codelab에서는 오프라인 캐시를 ViewModel
과 통합하여 네트워크에서 재생목록을 가져오는 대신 저장소의 재생목록을 표시했습니다.
7. 솔루션 코드
솔루션 코드
Android 스튜디오 프로젝트: RepositoryPattern
8. 축하합니다
축하합니다. 이 개발자 과정에서 배운 내용은 다음과 같습니다.
- 캐싱은 네트워크에서 가져온 데이터를 기기의 저장소에 저장하는 프로세스입니다. 캐싱을 사용하면 기기가 오프라인 상태일 때 또는 앱이 같은 데이터에 다시 액세스해야 하는 경우 앱에서 데이터에 액세스할 수 있습니다.
- 앱이 기기의 파일 시스템에 구조화된 데이터를 저장하는 가장 좋은 방법은 로컬 SQLite 데이터베이스를 사용하는 것입니다. Room은 SQLite 객체 매핑 라이브러리로, SQLite를 통해 추상화 레이어를 제공합니다. 오프라인 캐싱을 구현하려면 Room을 사용하는 것이 좋습니다.
- 저장소 클래스는 Room 데이터베이스 및 웹 서비스와 같은 데이터 소스를 앱의 나머지 부분과 분리합니다. 저장소 클래스는 앱의 나머지 부분의 데이터 액세스를 위한 깔끔한 API를 제공합니다.
- 코드 분리와 아키텍처에 저장소를 사용하는 것이 좋습니다.
- 오프라인 캐시를 설계할 때는 앱의 네트워크와 도메인, 데이터베이스 객체를 분리하는 것이 좋습니다. 이 전략은 관심사 분리를 보여주는 예시입니다.