안드로이드의 리포지토리 패턴(Repository Pattern)
안드로이드의 리포지토리 패턴(Repository Pattern)
리포지토리 패턴(Repository Pattern)은 데이터 접근 로직을 깔끔한 API 뒤에 추상화하는 디자인 패턴입니다. 애플리케이션의 나머지 부분이 데이터를 어떻게 가져오고, 캐싱하고, 영속화하는지에 대한 세부 구현에 의존하지 않도록 분리해 줍니다. 안드로이드에서 리포지토리는 ViewModel과 데이터 소스(네트워크 API, 로컬 데이터베이스, 인메모리 캐시) 사이에 위치하며, 데이터 작업을 위한 단일 진입점(entry point) 역할을 수행합니다. 실무에서 안드로이드 프로젝트의 데이터 레이어를 설계할 때 가장 기본이 되는 패턴이므로, 면접에서도 자주 출제되는 주제 중 하나입니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- 데이터 소스와 소비자 사이에서 리포지토리가 수행하는 중재자(mediator) 역할을 설명할 수 있습니다.
- 로컬 및 원격 소스를 활용한 오프라인 우선(offline-first) 데이터 전략을 리포지토리 패턴이 어떻게 구현하는지 이해할 수 있습니다.
- 로컬 데이터베이스를 정규 데이터 저장소로 활용하는 단일 진실 공급원(Single Source of Truth) 원칙을 적용할 수 있습니다.
- suspend 함수와
Flow를 노출하는 리포지토리 인터페이스를 설계할 수 있습니다. - 의존성 주입(dependency injection)을 통해 가짜(fake) 데이터 소스를 주입함으로써 리포지토리를 테스트할 수 있습니다.
데이터 중재자로서의 리포지토리
리포지토리는 어떤 데이터 소스에 질의할지, 결과를 어떻게 캐싱할지, 로컬과 원격 소스 간 동기화를 어떻게 수행할지 결정하는 로직을 캡슐화합니다. ViewModel은 리포지토리의 메서드를 하나 호출하기만 하면 데이터를 받을 수 있으며, 그 데이터가 네트워크에서 왔는지, 데이터베이스에서 왔는지, 인메모리 캐시에서 왔는지 알 필요가 없습니다. 이러한 구조 덕분에 각 계층의 책임이 명확하게 분리되어, 코드베이스의 유지보수성이 크게 높아집니다.
interface ArticleRepository {
fun getArticles(): Flow<List<Article>>
suspend fun refreshArticles()
}
위의 인터페이스는 두 가지 연산을 노출합니다. 현재 아티클 목록과 이후 업데이트를 방출하는 Flow, 그리고 네트워크에서 새로운 데이터를 가져오도록 트리거하는 suspend 함수가 그것입니다. ViewModel은 Retrofit이나 Room, 또는 캐싱 로직에 대해 전혀 알지 못하며, 오직 이 인터페이스에만 의존합니다. 이처럼 인터페이스를 통해 계약을 정의하는 것이 리포지토리 패턴의 핵심입니다.
이러한 디커플링은 세 가지 이점을 제공합니다. 첫째, ViewModel을 실제 네트워크나 데이터베이스 의존성 없이 테스트할 수 있습니다. 둘째, 데이터 소스 구현체를 변경하더라도(Retrofit에서 Ktor로, 혹은 Room에서 SQLDelight로 교체) ViewModel 코드를 수정할 필요가 없습니다. 셋째, 캐싱과 동기화 로직이 한곳에 집중되어 있으므로, 여러 ViewModel에 분산되는 문제를 방지할 수 있습니다. 면접에서 이 세 가지 이점을 명확히 설명하실 수 있다면 좋은 평가를 받으실 것입니다.
단일 진실 공급원(Single Source of Truth)
단일 진실 공급원(Single Source of Truth) 패턴은 로컬 데이터베이스를 정규 데이터 저장소로 사용합니다. 네트워크는 주요 데이터 소스가 아니라 동기화 수단으로 취급됩니다. 리포지토리는 네트워크 응답을 데이터베이스에 기록하고, 데이터베이스를 Flow로 UI 레이어에 노출합니다. 이 원칙을 이해하는 것은 안드로이드 데이터 레이어 설계에서 매우 중요합니다.
class ArticleRepositoryImpl(
private val api: ArticleApi,
private val dao: ArticleDao
) : ArticleRepository {
override fun getArticles(): Flow<List<Article>> {
return dao.observeAll() // 로컬 DB를 Flow로 관찰
}
override suspend fun refreshArticles() {
withContext(Dispatchers.IO) {
val remote = api.fetchArticles() // 네트워크에서 데이터 가져오기
dao.insertAll(remote) // 가져온 데이터를 로컬 DB에 저장
}
}
}