Android 프로젝트를 멀티 모듈 구조로 구성할 때 가장 자주 사용하는 형태는 다음과 같다.

Domain 모듈이 순수 Kotlin 모듈인 것은 다들 아는 사실일 것이다.
그런데, Data 모듈은 둘 다 될 수도 있다고 한다.
여기에 추가로 알 수 있는 사항은 Android 의존성 없이 JVM 기반으로 유지하면 테스트 환경이 가벼워지고, 순수한 비즈니스 로직만 다루도록 설계할 수 있다는 점이다.
나 역시 초기에는 Ktor를 사용해 네트워크 통신을 구현했기 때문에 Data 모듈을 Kotlin JVM 모듈로 구성했다.
Ktor는 멀티플랫폼을 지원하고 Android 종속성이 없는 client-core, client-cio 엔진만 사용하면 JVM 환경에서도 문제 없이 동작한다.
그러나 Paging3를 도입하면서 상황이 달라졌다.
Ktor 기반으로 Repository를 구성하던 시점에는 다음과 같은 구조가 가능했다.
이 경우 Data 모듈에는 Android 종속성이 전혀 없기 때문에 kotlin("jvm") 모듈로 구현해도 문제가 없다.
하지만 무한 스크롤 기능을 위해 Paging3를 도입하면서 제약이 생긴다.
Paging3는 다음과 같이 세 개의 모듈로 나뉜다.
여기서 핵심은 Pager 클래스가 paging-runtime에 포함되어 있다는 점이다.
Repository에서 Pager를 사용해 PagingData 스트림을 생성해야 하는데, 이 시점에서 Android 의존성이 필요해진다.
예시:
fun getCharacters(): Flow<PagingData<Character>> {
return Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = { CharacterPagingSource(api) }
).flow
}
Pager는 내부적으로 AndroidX 라이브러리와 연동되어 있고, paging-runtime 또한 AAR(Android Archive) 형태로 배포된다.
따라서 다음과 같은 문제가 발생한다.
즉, Repository에서 Pager를 생성하는 순간 Data 모듈은 반드시 Android 모듈이어야 한다.
일부 개발자는 Pager 생성을 Presentation 계층에서 수행하여 Data 모듈을 순수 Kotlin 모듈로 유지할 수 있다고 생각할 수 있다.
그러나 이는 클린 아키텍처 관점에서 좋지 않은 설계다.
Pager는 다음 역할을 수행한다.
이 로직은 "UI 로직"이 아니라 "데이터 로딩 전략"에 속한다.
즉, Repository가 책임져야 하는 부분이다.
Presentation 계층으로 Pager 로직을 밀어 넣으면 UI와 데이터 로직이 결합되어 아키텍처가 무너진다.
따라서 다음이 가장 자연스러운 구조다.
이 구조를 유지하려면 Data 모듈은 Android Library 모듈이어야 한다.
즉, Paging3를 올바르게 사용하기 위해서는 Data 모듈이 Android 모듈로 변경되는 것이 불가피하며, 오히려 아키텍처 관점에서도 더 자연스러운 선택이다.
이 변화는 단순히 모듈 타입을 변경하는 것 이상의 의미가 있다.
Android 환경에서 멀티 모듈 아키텍처를 사용한다면, 기술 스택에 따라 모듈의 성격이 달라질 수 있고 개발자는 그 이유와 구조적 적합성을 이해해야 한다.