android-paging 실행 context? - (paging 삽질기)

동키·2025년 2월 5일

안드로이드

목록 보기
1/14
post-thumbnail

안녕하세요 동키입니다.
오늘은 Compose로 개인 프로젝트를 진행하다 가진 궁금증을 공유하고자 합니다.

페이징이란
대용량 데이터셋을 한 번에 모두 메모리에 로드하지 않고, 필요한 만큼만 페이지 단위로 불러와서 화면에 표시하는 방식입니다.

코드 구조는 위와 같습니다.


Repository

class SearchPlaceRepositoryImpl @Inject constructor(
    private val searchPlaceDataSource: SearchPlaceDataSource
) : SearchPlaceRepository {
    override suspend fun getSerachResult(): Flow<PagingData<Poi>> {
        return Pager(
            config = PagingConfig(pageSize = 10, initialLoadSize = 10),
            pagingSourceFactory = { SearchPlacePagingSource(searchPlaceDataSource = searchPlaceDataSource) }
        ).flow
    }
}

우선 Repository에서 Pager flow를 반환합니다.

ViewModel

val searchFlow: Flow<PagingData<Poi>> = _searchQuery
        .debounce(500L)
        .distinctUntilChanged()
        .flatMapLatest { query ->
            searchPlaceUseCase(query).cachedIn(viewModelScope)
        }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), PagingData.empty())

searchPlaceUseCase는 SearchPlaceRepository의 getSearchResult함수를 호출합니다.

PagingSource

 override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Poi> {
        try {
            val page:Int = params.key?:1
            val loadSize = params.loadSize

            val searchPlaceList = withContext(context = Dispatchers.IO) {
                Log.d("searchjob", Thread.currentThread().name)
                //network 작업 실행
            }
            Log.d("SearchPlacePagingSource", Thread.currentThread().name)
            return LoadResult.Page(
                data = searchPlaceList,
                prevKey = null,
                nextKey = if (searchPlaceList.size == params.loadSize) page + 1 else null
            )
        } catch (e: Exception) {
            return LoadResult.Error(e)
        } 
    }

코드 구현은 이렇습니다.

문제점

PagingSource의 코드에서 Log를 통해서 실행되고 있는 스레드를 출력해 봤습니다.
(본인이 의도한대로 동작하는지 확인하기 위해)

보통 네트워크 작업을 진행할 땐 Dispatchers.IO를 사용하기 때문에 withContext를 통해 네트워크 작업 context를 Dispatchers.IO로 변경했습니다.

 DefaultDispatcher-worker-1

네트워크 작업이 DefaultDispatcher에서 실행된 것을 확인할 수 있습니다.

그러나 Log.d("SearchPlacePagingSource", Thread.currentThread().name)

main

네트워크 작업이 끝난 뒤 load 함수의 컨텍스트가 main인 것을 확인할 수 있었습니다.

"음 페이징 load 함수 자체도 IO에서 진행되어야 하는거 아닌가?"
라는 생각을 하게 되었습니다.

그래서 바로 코드를 수정하고 결과를 확인해봤습니다.

fun searchQuery(query: String) {
        viewModelScope.launch(context = Dispatchers.IO) {
            val searchFlow = searchPlaceUseCase(query)
                .stateIn(viewModelScope, SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), PagingData.empty())
            withContext(context = Dispatchers.Main) {
                _searchQuery.update { it.copy(places = searchFlow) }
            }
        }
    }

음 launch의 context를 Dispatchers.IO로 수정했으니 이제 main이 안나오겠지?

DefaultDispatcher-worker-2
main

??

결과는 이전과 같이 withContext로 IO로 설정한 네트워크 작업은 DefaultDispatcher를 사용하지만 네트워크 작업이 끝난 뒤의
load 함수는 여전히 main에서 실행되고 있었습니다.

"음 뭐지?" 하고 flowOn(Dispatchers.IO)을 모든 곳에 걸어줘봤습니다.

val searchFlow = searchPlaceUseCase(query)
                .flowOn(context = Dispatchers.IO)
                .stateIn(viewModelScope, SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), PagingData.empty())

이렇게도 해보고

override suspend fun getSearchResult(): Flow<PagingData<Poi>> {
        return Pager(
            config = PagingConfig(pageSize = 10, initialLoadSize = 10),
            pagingSourceFactory = { SearchPlacePagingSource(searchPlaceDataSource = searchPlaceDataSource) }
        ).flow
            .flowOn(context = Dispatchers.IO)
    }

이렇게도 해봤지만 결과는 같았습니다.
그렇게 계속 삽질을 하더 도중 지쳐....

"음... 그래도 네트워크 작업은 IO에서 진행되었으니까 데이터가 load 되는 과정은 main에서 진행되도 괜찮겠지?" 생각이 들었습니다.

그래서 GPT에게 바로 질문...

본적으로 네트워크와 같이 부하가 큰 작업은withContext(Dispatchers.IO) 등을 사용해서 별도의 IO 스레드에서 처리하는 것이 핵심입니다. 이 경우 PagingSource의 load() 함수 내에서 네트워크 호출은 IO 스레드에서 안전하게 이루어지고, 그 이후의 가볍고 단순한 작업(데이터 포장, LoadResult.Page 생성 등)은 Main 스레드에서 실행되어도 문제가 없습니다

음 그래도 왜 계속 main이 찍히는지 궁금증을 해결하지는 못한 것 같습니다 ㅠㅠ
gpt가 틀릴 수도 있으니 ... Paging 내부 코드를 보면서 더 공부를 해야할 것 같습니다.

혹시 아시는 분이 있다면 알려주시면 감사하겠습니다(훈수 환영)!!

profile
오늘 하루도 화이팅

0개의 댓글