[Andorid] Cursor 기반 페이징, Offset 기반 페이징 그리고 Paging 라이브러리에 대해 알아보자

Dec 20, 2024
[Andorid] Cursor 기반 페이징, Offset 기반 페이징 그리고 Paging 라이브러리에 대해 알아보자
 

Paging

페이징은 대량의 데이터를 효율적으로 처리하고 사용자에게 보다 나은 경험을 제공하기 위해 필수적인 기법이다.
특히, Android 애플리케이션 개발 시 데이터베이스에서 데이터를 가져오는 방식은 성능과 사용자 경험에 큰 영향을 미친다.
여기서는 Cursor 기반 페이징과 Offset 기반 페이징의 차이를 비교하여 각각의 장단점을 살펴보자. 그리고 안드로이드 앱에 페이징 라이브러리를 적용되는 과정을 살펴보자.
 

Offset 기반 페이징

Offset 기반 페이징은 클라이언트가 페이지 번호와 페이지당 요청할 데이터 수를 지정하여 데이터를 조회하는 방식입니다. 이 방식은 SQL 쿼리에서 LIMITOFFSET을 사용하여 구현됩니다.
장점
  • 단순한 구현: Offset 기반 페이징은 구현이 간단하며, 사용자가 특정 페이지로 직접 이동할 수 있습니다. 예를 들어, 사용자가 10페이지로 바로 이동할 수 있습니다.
  • 유연한 정렬: 다양한 정렬 방식을 쉽게 적용할 수 있습니다.
단점
  • 성능 저하: 데이터 양이 많아질수록 높은 Offset 값을 사용할 경우 성능이 저하됩니다. 데이터베이스는 요청된 페이지에 도달하기 위해 많은 데이터를 스캔해야 하므로, 후반 페이지로 갈수록 속도가 느려집니다.
  • 중복 및 누락 문제: 데이터가 자주 추가되거나 삭제되는 경우, 사용자가 페이지를 넘길 때 중복된 데이터가 나타나거나 누락될 수 있습니다.
 
 

Cursor 기반 페이징

Cursor 기반 페이징은 데이터의 마지막 항목을 기준으로 다음 데이터를 가져오는 방식입니다. 이 방법은 주로 고유 식별자(예: ID)를 사용하여 구현됩니다.
장점
  • 우수한 성능: Cursor 기반 페이징은 고속의 데이터 검색을 가능하게 합니다. 높은 Offset 값을 사용하는 대신, 이전에 조회한 마지막 항목을 기준으로 다음 항목을 가져오기 때문에 데이터베이스의 부하가 적습니다.
  • 일관성 있는 결과: 데이터가 추가되거나 삭제되더라도 이전에 조회한 데이터를 기준으로 결과를 반환하므로, 사용자에게 일관된 결과를 제공합니다.
단점
  • 복잡한 구현: Cursor 기반 페이징은 구현이 복잡하며, 클라이언트가 매번 커서를 관리해야 합니다. 또한, 특정 페이지로 직접 이동할 수 없고, 오직 다음 또는 이전 페이지로만 이동할 수 있습니다.
  • 제한된 유연성: 복잡한 정렬 조건을 처리하는 데 어려움이 있을 수 있으며, 특정 데이터 집합에 대해 직접 접근하기 어렵습니다.
안드로이드 페이징 라이브러리도 Cursor 기반 페이징을 사용한다.
 
결론은 애플리케이션의 요구 사항에 따라 적절한 방식을 선택하는 것이 중요하다. Offset 기반 페이징은 단순하고 직관적인 방법으로 소규모 데이터셋에 적합하다. Cursor 기반 페이징은 대규모 데이터셋이나 실시간으로 변화하는 데이터에 적합하며, 성능과 일관성을 중시하는 경우에 유리하다. 따라서, Android 애플리케이션 개발 시에는 이러한 특성을 고려하여 최적의 페이징 방식을 선택하는 것이 필요하다.
 
 

Paging Library 사용하기

Paging 라이브러리를 사용하면 로컬 저장소에서나 네트워크를 통해 대규모 데이터 세트의 데이터 페이지를 로드하고 표시할 수 있다. 안드로이드에서 페이징을 적용하려면 Paging 라이브러리를 종속성에 추가하고 사용해야 합니다. 페이징 라이브러리는 대량의 데이터를 효율적으로 처리하기 위해 설계된 Jetpack의 일부로, 데이터를 페이지 단위로 로드하여 메모리 사용을 최적화한다.
 

페이징 라이브러리를 사용하여 얻을 수 있는 이점

  • Paging된 데이터의 메모리 내 캐싱. 이렇게 하면 앱이 Paging 데이터로 작업하는 동안 시스템 리소스를 효율적으로 사용할 수 있습니다.
  • 요청 중복 삭제 기능이 기본 제공되므로 앱에서 네트워크 대역폭과 시스템 리소스를 효율적으로 사용할 수 있습니다.
  • 사용자가 로드된 데이터의 끝까지 스크롤할 때 구성 가능한 RecyclerView 어댑터가 자동으로 데이터를 요청합니다.
  • Kotlin Coroutine 및 Flow뿐만 아니라 LiveData 및 RxJava를 최고 수준으로 지원합니다.
  • 새로고침 및 재시도 기능을 포함하여 오류 처리를 기본으로 지원합니다.
 
 

종속성 추가하기

dependencies { val paging_version = "3.3.5" implementation("androidx.paging:paging-runtime:$paging_version") // alternatively - without Android dependencies for tests testImplementation("androidx.paging:paging-common:$paging_version") // optional2️⃣ - RxJava2 support implementation("androidx.paging:paging-rxjava2:$paging_version") // optional2️⃣ - RxJava3 support implementation("androidx.paging:paging-rxjava3:$paging_version") // optional3️⃣ - Guava ListenableFuture support implementation("androidx.paging:paging-guava:$paging_version") // optional4️⃣ - Jetpack Compose integration implementation("androidx.paging:paging-compose:3.3.5") }
2️⃣ RxJava2, RxJava3과 함께 Paging 라이브러리를 사용하고자 할 때 추가합니다. RxJava2를 사용하여 반응형 프로그래밍 패러다임으로 페이징을 구현할 수 있습니다. RxJava의 최신 버전을 선호하는 경우 사용합니다.
3️⃣ Google의 Guava 라이브러리의 ListenableFuture와 함께 Paging을 사용하고자 할 때 이 종속성을 추가합니다
4️⃣ Jetpack Compose를 사용하여 UI를 구축하는 경우, 이 종속성을 추가하여 Paging 라이브러리와 Compose를 함께 사용할 수 있습니다
 

페이징 라이브러리 아키텍처

notion image
  • The repository layer : 저장소 레이어
  • The ViewModel layer : 뷰모델 레이어
  • The UI layer : UI 레이어
 

Repository layer

리포지토리 계층의 기본 페이징 라이브러리 구성 요소는 PagingSource입니다. 각 PagingSource 개체는 데이터 소스와 해당 소스에서 데이터를 검색하는 방법을 정의합니다. PagingSource 객체는 네트워크 소스 및 로컬 데이터베이스를 포함한 모든 단일 소스에서 데이터를 로드할 수 있습니다.
사용할 수 있는 또 다른 페이징 라이브러리 구성 요소는 RemoteMediator입니다. RemoteMediator 객체는 로컬 데이터베이스 캐시가 있는 네트워크 데이터 소스와 같은 계층화된 데이터 소스에서 페이징을 처리합니다.
 

ViewModel layer

Pager 구성요소는 PagingSource 객체 및 PagingConfig 구성 객체를 바탕으로 반응형 스트림에 노출되는 PagingData 인스턴스를 구성하기 위한 공개 API를 제공합니다.
ViewModel 레이어를 UI에 연결하는 구성요소는 PagingData입니다. PagingData 객체는 페이지로 나눈 데이터의 스냅샷을 보유하는 컨테이너입니다. PagingSource 객체를 쿼리하여 결과를 저장합니다.
 

UI layer

UI 레이어의 기본 페이징 라이브러리 구성요소는 페이지로 나눈 데이터를 처리하는 RecyclerView 어댑터인 PagingDataAdapter입니다.
또는 포함된 AsyncPagingDataDiffer 구성요소를 사용하여 고유한 맞춤 어댑터를 빌드할 수 있습니다.
참고: 앱에서 UI에 Compose를 사용하는 경우 Paging을 UI 레이어에 통합할 때 androidx.paging:paging-compose 아티팩트를 사용하세요. 자세한 내용은 collectAsLazyPagingItems()에 관한 API 문서를 참조하세요.
// androidx.paging.compose @Composable fun <T : Any> Flow<PagingData<T>>.collectAsLazyPagingItems( context: CoroutineContext = EmptyCoroutineContext ): LazyPagingItems<T>
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.sp import androidx.paging.LoadState import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.compose.collectAsLazyPagingItems val myBackend = remember { TestBackend(DATA) } val pager = remember { Pager( PagingConfig( pageSize = myBackend.DataBatchSize, enablePlaceholders = true, maxSize = 200 ) ) { myBackend.getAllData() } } val lazyPagingItems = pager.flow.collectAsLazyPagingItems() LazyColumn { if (lazyPagingItems.loadState.refresh == LoadState.Loading) { item { Text( text = "Waiting for items to load from the backend", modifier = Modifier.fillMaxWidth() .wrapContentWidth(Alignment.CenterHorizontally) ) } } items(count = lazyPagingItems.itemCount) { index -> val item = lazyPagingItems[index] Text("Index=$index: $item", fontSize = 20.sp) } if (lazyPagingItems.loadState.append == LoadState.Loading) { item { CircularProgressIndicator( modifier = Modifier.fillMaxWidth() .wrapContentWidth(Alignment.CenterHorizontally) ) } } }
 

Codelab

 
 

Paging 라이브러리의 기본 사용법

Paging 데이터 로드 및 표시

네트워크 및 데이터베이스에서의 Paging

 
Share article

code-with-me