PagingSource가 페이지 기능을 위해 DataSource로 정의하고 API와 직접적으로 통신하는 기능이었다면, RemoteMediator는 중간에 Room DB를 거쳐 UI에게 전달된다.

기존 Database(서버 API)에서 PagingSource를 통해 바로 ViewModel → UI로 흘러가는 것을 볼 수 있다.
반면 RemoteMediator는 다음과 같이 중간에 Room Databasae를 거쳐 전달된다.

PagingSource만 사용하는 것은 동일한 쿼리에 대해 매번 API 호출을 하기 때문에 불필요한 리소스가 소모될 수 있는 반면에, RemoteMediator를 사용하면 이전에 API에서 호출 된 적 있는 데이터는 캐싱 된 Room DB에서 호출하여 API 호출을 최소화 할 수 있다.
1. Room DB
데이터 캐싱에 필요한 Entity를 생성하고 저장, 삽입, 업데이트, 삭제 Dao를 구현하여 RemoteMediator 객체에서 사용해야 한다.
해당 예제는 사이드 프로젝트에서 진행한 공공API를 활용한 주소 검색 기능을 참고하였다.
@Entity
data class AddressRemoteKey(
@PrimaryKey val query: String,
val nextKey: Int?
)
RemoteKeyEntity의 경우 검색 Query에 대해 몇 번째 페이지까지 API 호출을 진행했는지 저장하는 Entity
@Entity
data class AddressEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val query: String,
val jibunAddr: String,
val roadAddr: String,
val name: String
)
API 호출로 불러온 데이터를 저장하는 Entity
@Dao
interface AddressEntityDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(addresses: List<AddressEntity>)
@Query("DELETE FROM AddressEntity WHERE `query` = :query")
suspend fun clearByQuery(query: String)
@Query(
"""
SELECT jibunAddr, roadAddr, name
FROM AddressEntity
WHERE `query` = :query
"""
)
fun getAddress(query: String): PagingSource<Int, Juso>
}
@Dao
interface AddressRemoteKeyDao {
@Query("SELECT * FROM AddressRemoteKey WHERE `query` = :query")
suspend fun getRemoteKey(query: String): AddressRemoteKey?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertKey(key: AddressRemoteKey)
@Query("DELETE FROM AddressRemoteKey WHERE `query` = :query")
suspend fun clearKey(query: String)
}
DAO에서 살펴봐야 할 것은 불러올 데이터를 PagingSource Type으로 불러온다는 것이다.
RemoteMediator에서는 PagingSource를 직접 정의하는 것이 아닌 Dao에서 반환하여 사용한다.
2. RemoteMediator
@OptIn(ExperimentalPagingApi::class)
class AddressRemoteMediator(
private val query: String,
private val database: DataBase,
private val addressEntityDao: AddressEntityDao,
private val remoteKeyDao: AddressRemoteKeyDao,
private val networkService: ApiService
) : RemoteMediator<Int, Juso>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Juso>
): MediatorResult {
return try {
val page = when (loadType) {
//처음 호출 또는 새로고침 시
LoadType.REFRESH -> {
ADDRESS_STARTING_PAGE
}
// 이전 페이지 호출
LoadType.PREPEND -> {
return MediatorResult.Success(endOfPaginationReached = true)
}
// 다음 페이지 호출
LoadType.APPEND -> {
val key = remoteKeyDao.getRemoteKey(query)
val nextKey = key?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = true)
nextKey
}
}
// API 호출
val response = networkService.getAddress(keyword = query, page = page)
val result = response.body()?.result
val juso = result?.juso ?: emptyList()
val endOfPaginationReached = juso.size < state.config.pageSize
// Room DB 저장(캐싱)
database.withTransaction {
if (loadType == LoadType.REFRESH) {
addressEntityDao.clearByQuery(query)
remoteKeyDao.clearKey(query)
}
val entities = juso.map {
AddressEntity(
query = query,
jibunAddr = it.jibunAddr,
roadAddr = it.roadAddr,
name = it.name
)
}
addressEntityDao.insertAll(entities)
remoteKeyDao.insertKey(
AddressRemoteKey(
query = query,
nextKey = if (endOfPaginationReached) null else page + 1
)
)
}
MediatorResult.Success(endOfPaginationReached)
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}
}
override suspend fun initialize(): InitializeAction {
val key = remoteKeyDao.getRemoteKey(query)
return if (key != null) {
InitializeAction.SKIP_INITIAL_REFRESH
} else {
InitializeAction.LAUNCH_INITIAL_REFRESH
}
}
companion object {
private const val ADDRESS_STARTING_PAGE = 1
private const val SUCCESS_CODE = "0"
}
}
- load() : API 호출로 데이터를 불러와 Room DB에 저장(캐싱)하는 함수
- 반환 값: 성공 - MediatorResult.Success(endOfPaginationReached: Boolean), 실패 - MediatorResult.Error
- endOfPaginationReached
- true: 더 이상 API에서 불러올 데이터가 없는 경우
- false: 더 불러올 데이터가 남아있음
- 그러나 true를 반환한다고 load의 호출이 영원히 끝나는 것은 아니다.
- endOfPaginationReached
- 반환 값: 성공 - MediatorResult.Success(endOfPaginationReached: Boolean), 실패 - MediatorResult.Error
- initialize() : Refresh를 결정하는 함수
데이터를 저장한 지 일정 시간이 지난 경우 Refresh를 한다와 같이 특정 조건에 Refresh를 결정하는 함수로 load 이전에 자동으로 호출된다.
// Refresh 생략
InitializeAction.SKIP_INITIAL_REFRESH
// Refresh 시작
InitializeAction.LAUNCH_INITIAL_REFRESH
3. Pager
Pager(
config = PagingConfig(pageSize = NETWORK_PAGE_SIZE, enablePlaceholders = false),
remoteMediator = AddressRemoteMediator(
query,
database,
addressEntityDao,
addressRemoteKeyDao,
service
),
pagingSourceFactory = { addressEntityDao.getAddress(query) }
).flow
Pager에는 remoteMediator를 추가하고, pagingSourceFactory는 PagingSource를 반환하는 Dao를 넣어준다.
RemoteMediator는 Room 캐싱을 사용하기 때문에 cachedIn을 사용하지 않는다.
'안드로이드 > Jetpack' 카테고리의 다른 글
| Jetpack Paging - PagingSource (0) | 2026.03.18 |
|---|---|
| Navigation Component (0) | 2025.07.31 |
| [DataBinding] BindingAdapter 함수 파라미터 전달 (0) | 2025.07.24 |
| [안드로이드] Navigation Component (0) | 2023.11.29 |
| [안드로이드] Jetpack (0) | 2023.11.24 |