[Android] Clean Architecture With Paging3

이진영·2025년 6월 25일
post-thumbnail

📦 Clean Architecture 기반 Android 프로젝트에서 Paging3 적용기

최근 Clean Architecture 기반의 Android 프로젝트를 진행하며,
대량의 데이터를 효율적으로 처리하기 위해 Paging 3 라이브러리를 도입하게 되었습니다.

하지만 Paging3는 기본적으로 Android 의존성을 전제로 설계되어 있기 때문에,
클린 아키텍처의 계층 분리 원칙에 맞춰 적용하기 위해 많은 고민이 필요했습니다.

이 글에서는 아래와 같은 고민에 대한 해답을 찾고자 했습니다:

  • 🧩 Android 비의존 모듈(domain/data)에서 Paging3를 어떻게 사용할 수 있을까?
  • 🛠️ UI와 비즈니스 로직의 역할을 명확히 분리하면서도 효율적인 페이징 구현이 가능할까?

📚 이 글에서 다룰 내용

  • paging-commonpaging-runtime, paging-compose의 차이점
  • Paging을 클린 아키텍처에 맞춰 적용하는 방법
  • Android 의존성을 분리하는 구조 설계 팁

🎯 목표

Paging3의 기본 구조를 이해하고,
클린 아키텍처의 각 계층에서 어떤 책임을 가져가야 하는지를 분명히 하여
확장 가능하고 유지보수하기 쉬운 구조를 설계하는 것이 목표입니다.


🧱 클린 아키텍처 계층별 역할과 Paging3 적용의 고민

클린 아키텍처에서는 다음과 같이 계층별로 명확한 책임 분리가 이루어집니다:

  • Presentation (프레젠테이션): UI 구성, 사용자 인터랙션 처리
  • Domain (도메인): 비즈니스 로직 및 UseCase
  • Data (데이터): Repository, DataSource, API/DB 통신 등

🤔 Paging3는 왜 도메인/데이터 계층에서 바로 사용할 수 없을까?

Paging 3는 기본적으로 Android 종속성을 가진 라이브러리입니다.
대표적으로 다음과 같은 Android 기반 API가 포함되어 있습니다:

  • PagingDataAdapter (RecyclerView 기반)
  • LazyPagingItems (Jetpack Compose 기반)
  • collectAsLazyPagingItems() (Jetpack Compose 기반)

👉 따라서 클린 아키텍처에서 도메인 계층이나 데이터 계층
순수 Kotlin/JVM 기반으로 구성되어야 하므로,
Android 종속적인 Paging3 컴포넌트는 직접 사용할 수 없습니다.


⚙️ 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")
}

📦 Android 종속성이 없는 paging-common

이를 해결하기 위해 Google은 paging-common이라는
Android 종속성이 없는 별도 Paging3 라이브러리를 제공합니다.

처음에는 테스트 전용 유틸처럼 보였지만, 공식적으로
"Android 종속성 없이" 가능하다는 점이 명시되어 있었습니다.

이를 통해 다음과 같은 핵심 컴포넌트를 Android 외부 모듈에서도 안전하게 사용할 수 있습니다:

  • PagingSource
  • PagingConfig
  • PagingData
  • Pager

✅ 계층별 Paging3 의존성 구성 가이드

Android 종속성 여부에 따라, 각 계층에는 적절한 Paging3 라이브러리를 선택적으로 통합해야 합니다:

계층사용 라이브러리Android 종속성설명
domainpaging-common❌ 없음비즈니스 로직 계층은 Android에 의존하지 않으므로 paging-common만 사용
datapaging-common❌ 없음Repository, PagingSource 구현 등도 Android-free 구성으로 유지
presentationpaging-runtime, paging-compose✅ 있음RecyclerView 또는 Compose UI에서 실제 페이징 처리 담당

📌 요약:
domaindata는 반드시 paging-common을 사용해야 하고,
Android UI와 관련된 처리만 paging-runtime 또는 paging-compose로 분리합니다.


🧱 Clean Architecture 기반 Movie Paging3 예시

Movie 데이터를 Paging3로 불러오는 과정을
클린 아키텍처 관점에서 간단한 예시로 정리해보겠습니다.


📂 data 계층

🔸 MoviePagingSource.kt

class 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.kt

interface 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
    }
}

📂 domain 계층

🔸 GetMoviePagingUseCase.kt

class GetMoviePagingUseCase @Inject constructor(
    private val repository: MovieRepository
) {
    operator fun invoke(): Flow<PagingData<Movie>> = repository.getMoviePaging()
}

📂 presentation 계층

🔸 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()
}

🧩 계층별 Paging3 클래스 및 의존성 정리

계층 (역할)사용 가능 클래스사용할 의존성
Domain
(UseCase)
PagingData, PagingConfigandroidx.paging:paging-common
Data
(Repository, API, DB)
PagingSource, PagingConfig, Pagerandroidx.paging:paging-common
Presentation
(ViewModel, UI)
PagingData, LazyPagingItems, collectAsLazyPagingItems()androidx.paging:paging-runtime
androidx.paging:paging-compose

✅ 마무리 정리

Clean Architecture에서 Paging3를 적용할 때 가장 중요한 포인트는
Android 종속성과 비종속성 컴포넌트를 정확히 구분하고,
각 계층에 맞는 책임과 역할을 분리하는 것입니다.


🧠 마지막으로

Paging3를 무작정 도입하는 것보다
각 계층의 의존성과 역할을 고려한 설계
유지보수성과 확장성을 크게 높여줍니다.

이 글이 Paging3를 클린하게 적용하려는 분들께
실질적인 구조 설계 가이드가 되었길 바랍니다.


🔗 관련 문서


profile
Android Developer

0개의 댓글