RemoteMediator와 캐싱
네트워크 페이징과 데이터베이스 페이징을 결합합니다.
RemoteMediator와 캐싱은(는) CoddyKit의 무료 Android Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Android Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Android Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
오프라인 우선 페이징
일반적인 네트워크 PagingSource도 작동하지만, 연결이 끊기면 목록이 비어 버립니다. 견고한 앱을 만들려면 데이터베이스를 단일 진실 공급원으로 사용하고 네트워크가 백그라운드에서 데이터베이스를 채우도록 해야 합니다.
바로 이 기능을 RemoteMediator가 제공합니다. 로컬 데이터베이스에서 페이지를 불러오고, 데이터베이스에 데이터가 부족해지면 네트워크에서 더 가져옵니다.
아키텍처
RemoteMediator를 사용하면 데이터가 한 방향으로 흐릅니다.
- UI는 Room의
PagingSource에서 페이지를 불러옵니다 - Room의 데이터가 끝에 가까워지면
RemoteMediator가 네트워크의 다음 페이지를 가져옵니다 - 네트워크 결과가 Room에 기록됩니다
- Room이 새 행을 내보내고 UI가 업데이트됩니다
따라서 오프라인 상태에서도 사용자는 캐시된 데이터를 항상 즉시 볼 수 있습니다.
// Network ---> RemoteMediator ---> Room (source of truth) ---> UI
// UI never reads the network directly자동으로 생성되는 Room PagingSource
Room은 PagingSource를 자동으로 생성할 수 있습니다. PagingSource<Int, Entity>를 반환하는 쿼리만 선언하면 Room이 이를 구현합니다.
이것이 실제로 UI가 페이지를 불러오는 데이터 원본입니다.
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Query
@Dao
interface ArticleDao {
@Query("SELECT * FROM articles ORDER BY position ASC")
fun pagingSource(): PagingSource<Int, ArticleEntity>
}RemoteMediator 상속하기
RemoteMediator<Key, Value>에는 필수 메서드인 load()와 선택 사항인 initialize()가 있습니다.
load()는 LoadType(REFRESH, PREPEND 또는 APPEND)과 현재 PagingState를 받아 MediatorResult를 반환합니다.
import androidx.paging.ExperimentalPagingApi
import androidx.paging.RemoteMediator
@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
private val api: ArticleApi,
private val db: AppDatabase
) : RemoteMediator<Int, ArticleEntity>() {
// override suspend fun load(loadType, state): MediatorResult
}LoadType 처리하기
load()에서는 LoadType에 따라 가져올 페이지를 결정합니다.
- REFRESH - 첫 페이지를 불러옵니다(또는 앵커 주변의 페이지를 불러옵니다)
- PREPEND - 일반적으로 수행할 작업이 없으므로
endOfPaginationReached = true를 반환합니다 - APPEND - 현재 가지고 있는 마지막 페이지 다음의 페이지를 불러옵니다
val page: Int = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND ->
return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastKey = db.remoteKeyDao().last()?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = true)
lastKey
}
}원격 키 테이블
PagingSource와 달리 RemoteMediator는 다음 키와 이전 키를 직접 반환할 수 없으므로 영속화해야 합니다. 데이터와 함께 작은 원격 키 테이블에 저장하세요.
이렇게 하면 앱을 다시 시작한 후에도 APPEND/PREPEND를 이어서 수행할 수 있습니다.
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "remote_keys")
data class RemoteKey(
@PrimaryKey val articleId: Int,
val prevKey: Int?,
val nextKey: Int?
)네트워크 결과를 Room에 기록하기
페이지를 가져온 후 항목과 해당 키를 트랜잭션 안에서 Room에 기록합니다. REFRESH에서는 먼저 기존 테이블을 삭제하여 캐시의 일관성을 유지합니다.
db.withTransaction {
if (loadType == LoadType.REFRESH) {
db.remoteKeyDao().clearAll()
db.articleDao().clearAll()
}
val nextKey = if (response.items.isEmpty()) null else page + 1
db.remoteKeyDao().insertAll(
response.items.map { RemoteKey(it.id, prevKey = page - 1, nextKey = nextKey) }
)
db.articleDao().insertAll(response.items.map { it.toEntity() })
}MediatorResult 반환하기
load()를 마칠 때 MediatorResult를 반환합니다.
- 더 이상 페이지가 없을 때는
Success(endOfPaginationReached = true) - 더 있을 수 있을 때는
Success(endOfPaginationReached = false) - 실패하면
LoadState.Error로 전달되는Error(e)
return try {
val response = api.getArticles(page = page, size = state.config.pageSize)
db.withTransaction { /* write items + keys */ }
MediatorResult.Success(
endOfPaginationReached = response.items.isEmpty()
)
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}remoteMediator로 Pager 연결하기
remoteMediator와 Room의 pagingSourceFactory를 모두 사용하여 Pager를 구성합니다. 중재자가 Room을 채우고 UI는 Room에서 페이지를 불러옵니다.
실험적 API를 사용하므로 @OptIn(ExperimentalPagingApi::class)으로 사용을 허용해야 합니다.
@OptIn(ExperimentalPagingApi::class)
fun articles(): Flow<PagingData<ArticleEntity>> = Pager(
config = PagingConfig(pageSize = 20),
remoteMediator = ArticleRemoteMediator(api, db),
pagingSourceFactory = { db.articleDao().pagingSource() }
).flowAPPEND에 사용할 키 조회하기
APPEND를 로드하려면 마지막으로 로드된 항목의 nextKey가 필요합니다. PagingState의 앵커를 사용하여 키 테이블에서 올바른 원격 키를 찾습니다.
도우미 함수로 load()를 REFRESH, APPEND, PREPEND에 걸쳐 읽기 쉽게 유지할 수 있습니다.
private suspend fun lastRemoteKey(
state: PagingState<Int, ArticleEntity>
): RemoteKey? {
return state.pages.lastOrNull { it.data.isNotEmpty() }
?.data?.lastOrNull()
?.let { db.remoteKeyDao().keyFor(it.id) }
}
// In load(): for APPEND
val nextKey = lastRemoteKey(state)?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = true)캐싱과 최신성
데이터가 Room에 저장되므로 앱이 즉시 로드되고 오프라인에서도 작동합니다. 오래된 콘텐츠를 피하려면 initialize()를 재정의하여 캐시가 얼마나 오래되었는지에 따라 시작할 때 새로고침할지 결정합니다.
override suspend fun initialize(): InitializeAction {
val lastUpdate = db.remoteKeyDao().lastUpdatedMillis() ?: 0L
val cacheTimeout = TimeUnit.HOURS.toMillis(1)
return if (System.currentTimeMillis() - lastUpdate >= cacheTimeout) {
InitializeAction.LAUNCH_INITIAL_REFRESH
} else {
InitializeAction.SKIP_INITIAL_REFRESH
}
}빠른 확인
RemoteMediator 설정에서 UI가 페이지를 불러오는 단일 진실 공급원은 무엇인가요?
복습: RemoteMediator와 캐싱
오프라인 우선 페이징 파이프라인을 구축했습니다.
- UI는 Room의
PagingSource에서 페이지를 불러옵니다. 이것이 단일 진실 공급원입니다 RemoteMediator.load()가REFRESH/PREPEND/APPEND를 처리하고 트랜잭션 안에서 결과를 Room에 기록합니다- 원격 키 테이블이 앱을 다시 시작해도 다음 키와 이전 키를 유지합니다
Pager는remoteMediator와pagingSourceFactory를 모두 사용합니다initialize()가 캐시의 최신 상태를 제어합니다
이제 온라인이나 오프라인에서 무한 목록을 효율적으로 로드할 수 있습니다. Paging 3 과정을 모두 마치신 것을 축하합니다.
자주 묻는 질문
“RemoteMediator와 캐싱” 강의는 무료인가요?
네 — “RemoteMediator와 캐싱” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Android Academy 강의 전체를 잠금 해제할 수 있습니다. Android Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“RemoteMediator와 캐싱”에서 뭘 배우나요?
네트워크 페이징과 데이터베이스 페이징을 결합합니다. 브라우저에서 직접 실행하는 실습 코드로 Android Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Android Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Android Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“RemoteMediator와 캐싱” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Android Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Android Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 페이징이 필요한 이유
- PagingSource와 Pager
- Compose 목록에서 페이징
- RemoteMediator와 캐싱