본문 바로가기

안드로이드/Jetpack

Jetpack Paging - RemoteMediator

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의 호출이 영원히 끝나는 것은 아니다.
  • 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