
Paging 3 RemoteMediator는 네트워크 API와 Room 캐시를 함께 쓸 때 필요한 연결 지점입니다. 단순히 서버에서 페이지를 받아오는 것과 달리, 로컬 DB를 단일 source of truth로 두려면 RemoteMediator의 역할을 이해해야 합니다.
핵심은 UI는 Room에서 읽고, RemoteMediator가 부족한 데이터를 네트워크에서 받아 DB에 채우는 구조입니다. 이렇게 하면 오프라인 캐시와 스크롤 페이징을 함께 다루기 쉬워집니다.

PagingSource만으로는 언제 부족할까
네트워크 API만 직접 호출하는 단순 목록이라면 PagingSource 하나로도 시작할 수 있습니다. 하지만 앱을 다시 열었을 때 이전 데이터가 보이거나, 오프라인에서도 목록을 보여주고 싶다면 로컬 DB가 필요합니다.
Paging 3 RemoteMediator의 역할
RemoteMediator는 Paging이 더 많은 데이터가 필요하다고 판단할 때 호출됩니다. 이때 서버에서 데이터를 가져오고, 그 결과를 Room에 저장합니다. UI는 서버 응답을 직접 보는 것이 아니라 Room의 PagingSource를 통해 데이터를 읽습니다.
@OptIn(ExperimentalPagingApi::class)
val pager = Pager(
config = PagingConfig(pageSize = 20),
remoteMediator = FeedRemoteMediator(api, database),
pagingSourceFactory = { database.feedDao().pagingSource() }
)왜 DB를 source of truth로 둘까
- 화면 회전이나 앱 재실행 후에도 캐시를 활용할 수 있다
- 네트워크와 UI 상태를 분리하기 쉽다
- Room query 변경이 UI에 자연스럽게 반영된다
- 오프라인 상태에서도 마지막 데이터를 보여줄 수 있다
remote key 테이블은 왜 필요할까
서버 API가 page number나 cursor를 사용한다면 앱은 다음에 어떤 값을 요청해야 하는지 기억해야 합니다. 이 정보를 item 테이블에 억지로 섞기보다 remote key 테이블로 분리하면 refresh, prepend, append 흐름을 관리하기 쉽습니다.
@Entity
data class FeedRemoteKey(
@PrimaryKey val feedId: String,
val prevKey: Int?,
val nextKey: Int?
)cursor 기반 API라면 Int 대신 String cursor를 저장할 수 있습니다. 중요한 것은 목록 데이터와 페이지 경계 정보를 구분하는 것입니다.
REFRESH, APPEND, PREPEND를 다르게 본다
RemoteMediator의 load는 LoadType에 따라 의미가 달라집니다. REFRESH는 처음 로드나 새로고침이고, APPEND는 아래로 더 불러오기입니다. PREPEND는 위쪽 데이터를 더 불러오는 흐름인데, 일반 피드에서는 쓰지 않는 경우도 많습니다.
- REFRESH: 캐시를 새 기준으로 다시 맞춘다
- APPEND: 마지막 item의 remote key로 다음 페이지를 요청한다
- PREPEND: 위 방향 페이지가 있는 구조에서 이전 데이터를 요청한다
- endOfPaginationReached: 더 불러올 데이터가 없는지 UI에 알려준다
LoadState는 어디에 쓰일까
Paging 목록에서는 처음 로딩, 다음 페이지 로딩, 새로고침, 에러가 서로 다릅니다. LoadState를 분리해서 보면 처음 화면 skeleton, 하단 loading, retry 버튼을 자연스럽게 나눌 수 있습니다.
val items = pager.collectAsLazyPagingItems()
when (items.loadState.refresh) {
is LoadState.Loading -> FullScreenLoading()
is LoadState.Error -> RetryScreen { items.retry() }
else -> FeedList(items)
}캐시 삭제 정책을 먼저 정한다
REFRESH 때 기존 데이터를 모두 지우면 최신성은 좋아 보이지만 화면이 비어 보일 수 있습니다. 반대로 오래된 데이터를 계속 유지하면 사용자는 낡은 목록을 최신 목록으로 오해할 수 있습니다. 그래서 캐시 만료 기준과 표시 정책을 함께 정해야 합니다.
- 새로고침 중 기존 데이터를 유지할지
- 에러가 나면 기존 캐시를 보여줄지
- 캐시가 오래되었음을 UI에 표시할지
- 로그아웃이나 계정 변경 때 어떤 테이블을 지울지
실무에서 자주 막히는 지점
- remote key 테이블을 어떻게 설계할지
- refresh 때 기존 데이터를 지울지 유지할지
- API page와 DB 정렬 기준이 어긋나지 않는지
- 에러가 났을 때 retry가 어떤 load를 다시 실행하는지
- 캐시 만료 정책을 어디에 둘지
테스트할 때 보는 기준
RemoteMediator는 네트워크, DB, paging 상태가 함께 얽혀 있어서 수동 확인만으로는 놓치기 쉽습니다. 최소한 refresh, append, error, empty response, end reached 흐름은 따로 확인하는 편이 좋습니다.
- 빈 DB에서 첫 페이지를 불러오는가
- 다음 페이지가 DB에 이어서 저장되는가
- 서버 에러 때 기존 캐시가 어떻게 보이는가
- 마지막 페이지에서 더 이상 append하지 않는가
- 계정 변경이나 필터 변경 때 remote key가 섞이지 않는가
RemoteMediator가 과한 경우
모든 목록에 RemoteMediator가 필요한 것은 아닙니다. 캐시가 필요 없고 서버 데이터만 즉시 보여주면 되는 화면이라면 네트워크 기반 PagingSource가 더 단순할 수 있습니다. 반대로 오프라인 표시, 검색 필터 캐시, 새로고침 정책이 중요해지면 RemoteMediator를 검토합니다.
- 임시 검색 결과: 단순 PagingSource가 충분할 수 있다
- 홈 피드와 알림 목록: Room cache와 RemoteMediator가 유리할 수 있다
- 사용자 계정별 데이터: remote key가 계정과 섞이지 않게 설계해야 한다
정리
Paging 3 RemoteMediator는 네트워크 페이징과 Room 캐시를 이어주는 조정자입니다. UI는 DB를 읽고, RemoteMediator는 부족한 데이터를 채우며, LoadState는 로딩과 에러 표현을 맡습니다.
공식 구조는 Android Developers Paging network and database 문서를 기준으로 확인할 수 있습니다. 목록 상태 처리는 Paging 3 LoadState 글과 함께 보면 좋습니다.