
최근 Clean Architecture 기반의 Android 프로젝트를 진행하며,
대량의 데이터를 효율적으로 처리하기 위해 Paging 3 라이브러리를 도입하게 되었습니다.
하지만 Paging3는 기본적으로 Android 의존성을 전제로 설계되어 있기 때문에,
클린 아키텍처의 계층 분리 원칙에 맞춰 적용하기 위해 많은 고민이 필요했습니다.
이 글에서는 아래와 같은 고민에 대한 해답을 찾고자 했습니다:
paging-common과 paging-runtime, paging-compose의 차이점 Paging3의 기본 구조를 이해하고,
클린 아키텍처의 각 계층에서 어떤 책임을 가져가야 하는지를 분명히 하여
확장 가능하고 유지보수하기 쉬운 구조를 설계하는 것이 목표입니다.
클린 아키텍처에서는 다음과 같이 계층별로 명확한 책임 분리가 이루어집니다:
Paging 3는 기본적으로 Android 종속성을 가진 라이브러리입니다.
대표적으로 다음과 같은 Android 기반 API가 포함되어 있습니다:
PagingDataAdapter (RecyclerView 기반)LazyPagingItems (Jetpack Compose 기반)collectAsLazyPagingItems() (Jetpack Compose 기반)👉 따라서 클린 아키텍처에서 도메인 계층이나 데이터 계층은
순수 Kotlin/JVM 기반으로 구성되어야 하므로,
Android 종속적인 Paging3 컴포넌트는 직접 사용할 수 없습니다.
dependencies {
val paging_version = "3.3.6"
implementation("androidx.paging:paging-runtime:$paging_version")
// DOMAIN
// alternatively - without Android dependencies for tests
testImplementation("androidx.paging:paging-common:$paging_version")
// optional - RxJava2 support
implementation("androidx.paging:paging-rxjava2:$paging_version")
// optional - RxJava3 support
implementation("androidx.paging:paging-rxjava3:$paging_version")
// optional - Guava ListenableFuture support
implementation("androidx.paging:paging-guava:$paging_version")
// PRESENTATION
// optional - Jetpack Compose integration
implementation("androidx.paging:paging-compose:3.3.4")
}
paging-common이를 해결하기 위해 Google은 paging-common이라는
Android 종속성이 없는 별도 Paging3 라이브러리를 제공합니다.
처음에는 테스트 전용 유틸처럼 보였지만, 공식적으로
"Android 종속성 없이" 가능하다는 점이 명시되어 있었습니다.
이를 통해 다음과 같은 핵심 컴포넌트를 Android 외부 모듈에서도 안전하게 사용할 수 있습니다:
PagingSourcePagingConfigPagingDataPagerAndroid 종속성 여부에 따라, 각 계층에는 적절한 Paging3 라이브러리를 선택적으로 통합해야 합니다:
| 계층 | 사용 라이브러리 | Android 종속성 | 설명 |
|---|---|---|---|
domain | paging-common | ❌ 없음 | 비즈니스 로직 계층은 Android에 의존하지 않으므로 paging-common만 사용 |
data | paging-common | ❌ 없음 | Repository, PagingSource 구현 등도 Android-free 구성으로 유지 |
presentation | paging-runtime, paging-compose | ✅ 있음 | RecyclerView 또는 Compose UI에서 실제 페이징 처리 담당 |
📌 요약:
domain과data는 반드시paging-common을 사용해야 하고,
Android UI와 관련된 처리만paging-runtime또는paging-compose로 분리합니다.
Movie 데이터를 Paging3로 불러오는 과정을
클린 아키텍처 관점에서 간단한 예시로 정리해보겠습니다.
MoviePagingSource.ktclass MoviePagingSource(
private val api: MovieApi
) : PagingSource<Int, Movie>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> {
return ...
}
override fun getRefreshKey(state: PagingState<Int, Movie>): Int {
...
}
}
MovieRepositoryImpl.ktinterface MovieRepository {
fun getMoviePaging(): Flow<PagingData<Movie>>
}
class MovieRepositoryImpl @Inject constructor(
private val api: MovieApi
) : MovieRepository {
override fun getMoviePaging(): Flow<PagingData<Movie>> {
return Pager(
config = PagingConfig(pageSize = 10),
pagingSourceFactory = { MoviePagingSource(api) }
).flow
}
}
GetMoviePagingUseCase.ktclass GetMoviePagingUseCase @Inject constructor(
private val repository: MovieRepository
) {
operator fun invoke(): Flow<PagingData<Movie>> = repository.getMoviePaging()
}
MovieViewModel.kt@HiltViewModel
class MovieViewModel @Inject constructor(
private val getMoviePagingUseCase: GetMoviePagingUseCase,
) : ViewModel() {
private val _movieListState: MutableStateFlow<PagingData<MovieData>> =
MutableStateFlow(value = PagingData.empty())
val movieListState: StateFlow<PagingData<MovieData>> = _movieListState
init {
viewModelScope.launch {
getMoviePagingUseCase.invoke(
num = 10
).cachedIn(viewModelScope)
.collect {
_movieListState.value = it
}
}
}
}
MovieRoute.kt@Composable
internal fun MovieRoute(
viewModel: MovieViewModel = hiltViewModel(),
) {
val movieListPagingItems: LazyPagingItems<MovieData> =
viewModel.movieListState.collectAsLazyPagingItems()
}
| 계층 (역할) | 사용 가능 클래스 | 사용할 의존성 |
|---|---|---|
| Domain (UseCase) | PagingData, PagingConfig | androidx.paging:paging-common |
| Data (Repository, API, DB) | PagingSource, PagingConfig, Pager | androidx.paging:paging-common |
| Presentation (ViewModel, UI) | PagingData, LazyPagingItems, collectAsLazyPagingItems() | androidx.paging:paging-runtimeandroidx.paging:paging-compose |
Clean Architecture에서 Paging3를 적용할 때 가장 중요한 포인트는
Android 종속성과 비종속성 컴포넌트를 정확히 구분하고,
각 계층에 맞는 책임과 역할을 분리하는 것입니다.
Paging3를 무작정 도입하는 것보다
각 계층의 의존성과 역할을 고려한 설계가
유지보수성과 확장성을 크게 높여줍니다.
이 글이 Paging3를 클린하게 적용하려는 분들께
실질적인 구조 설계 가이드가 되었길 바랍니다.