Repository Pattern — 비즈니스 로직과 데이터 소스 사이에 추상화 계층을 추가하는 패턴입니다. API, 데이터베이스 또는 캐시를 직접 호출하는 대신 Repository는 데이터 획득 및 저장을 위한 통합 인터페이스를 제공합니다. 이는 테스트와 소스 간 전환을 간소화합니다. 자세한 내용은 Android Data Layer 문서를 참조하세요.
핵심 포인트
Repository Pattern은 비즈니스 로직을 데이터 소스에 대한 직접 액세스로부터 격리하는 구조적 패턴입니다. Activity, UIViewController 또는 ViewModel이 직접 Retrofit, URLSession, Room 또는 CoreData를 호출하는 대신 Repository와 통신합니다. Repository는 네트워크, 데이터베이스 또는 캐시 중 어디에서 데이터를 가져올지 결정하고 통합된 형식으로 결과를 반환합니다. 이는 단일 책임 원칙을 구현합니다 — UI는 데이터가 어떻게 또는 어디서 획득되었는지 알지 못합니다.
Repository 구성 요소에는 인터페이스(프로토콜), 구현 및 하나 이상의 DataSource가 포함됩니다. DataSource는 단일 소스로 작업하는 클래스입니다: RemoteDataSource는 HTTP 클라이언트를 통해 API를 호출하고, LocalDataSource는 데이터베이스를 읽고 씁니다. Repository는 생성자(의존성 주입)를 통해 DataSource를 받고 사용할 소스를 결정합니다. 예를 들어, 사용자 목록을 요청할 때 Repository는 먼저 캐시, 그다음 데이터베이스, 그다음 네트워크를 확인합니다.
Repository Pattern의 이점: 데이터 소스 변경(API 변경, DB 마이그레이션)이 UI 계층에 영향을 미치지 않음; Repository 또는 DataSource 대체를 통한 단위 테스트; UI에 투명한 캐싱; 화면 로직 변경 없이 온라인 및 오프라인 모드 간 전환. Android 커뮤니티는 Clean Architecture에서 Repository를 필수 계층으로 권장합니다.
iOS 구현에서 Repository는 Swift 프로토콜을 기반으로 구축됩니다. Repository 프로토콜은 데이터 획득 및 저장 메서드를 선언합니다. 실제 구현은 초기화자를 통해 주입됩니다 — 이를 통해 테스트 및 SwiftUI 미리보기에서 구현을 대체할 수 있습니다. DataSource도 프로토콜로 선언됩니다: Protocol RemoteDataSource, Protocol LocalDataSource. ViewModel 또는 Interactor는 특정 구현을 알지 못합니다 — Repository 프로토콜만 알 수 있습니다.
protocol UserRepository {
func getUsers() async throws -> [User]
}
protocol UserRemoteDataSource {
func fetchUsers() async throws -> [User]
}
protocol UserLocalDataSource {
func getCachedUsers() throws -> [User]
func saveUsers(_: [User]) throws
}
final class UserRepositoryImpl: UserRepository {
private let remote: UserRemoteDataSource
private let local: UserLocalDataSource
init(remote: UserRemoteDataSource, local: UserLocalDataSource) {
self.remote = remote
self.local = local
}
func getUsers() async throws -> [User] {
if let cached = try? local.getCachedUsers() {
return cached
}
let users = try await remote.fetchUsers()
try local.saveUsers(users)
return users
}
}
의존성 주입은 iOS에서 Repository를 위해 일반적으로 팩토리 또는 DI 컨테이너(Swinject, Factory)를 통해 구성됩니다. 테스트에서 UserRepository 프로토콜은 미리 정의된 데이터를 반환하는 모의 구현으로 대체됩니다. Async-await는 클로저와 델리게이트 없이 코드를 동기적이고 읽기 쉽게 만듭니다. Combine 반응성을 위해 Repository 메서드는 async throws 대신 AnyPublisher를 반환합니다.
Android 구현에서 Repository는 비동기 작업을 위해 Kotlin Coroutines 및 Flow를 광범위하게 사용합니다. Google은 공식 Android 아키텍처 가이드(Android Architecture Components)에서 Repository를 권장합니다. Repository는 생성자를 통해 RemoteDataSource(Retrofit) 및 LocalDataSource(Room)를 받고, ViewModel은 Repository의 Flow를 구독합니다. Repository는 데이터 전략을 관리합니다: 캐시 우선, 네트워크 우선, 또는 항상 네트워크와 캐시 쓰기.
interface UserRepository {
fun getUsers(): Flow<Result<List<User>>>
}
interface UserRemoteDataSource {
suspend fun fetchUsers(): List<User>
}
interface UserLocalDataSource {
fun getCachedUsers(): Flow<List<User>>
suspend fun saveUsers(users: List<User>)
}
class UserRepositoryImpl(
private val remote: UserRemoteDataSource,
private val local: UserLocalDataSource
) : UserRepository {
override fun getUsers(): Flow<Result<List<User>>> = flow {
emit(Result.Loading)
local.getCachedUsers().collect { cached ->
if (cached.isNotEmpty()) {
emit(Result.Success(cached))
}
}
try {
val users = remote.fetchUsers()
local.saveUsers(users)
emit(Result.Success(users))
} catch (e: Exception) {
emit(Result.Error(e))
}
}
}
Result 래퍼는 위 예제에서 Android의 표준입니다: sealed 클래스 Result가 ViewModel에 로딩 상태(Loading, Success, Error)를 알립니다. ViewModel은 collect를 통해 구독하고 StateFlow 또는 LiveData를 업데이트합니다. Flow를 사용하는 Repository는 데이터베이스 변경 사항을 UI에 자동으로 알립니다 — 이는 수동 새로고침 없이는 UI가 변경을 알 수 없는 일회성 요청과의 주요 차이점입니다.
DataSource — 특정 데이터 소스로 작업을 담당하는 클래스입니다. RemoteDataSource는 HTTP 클라이언트(URLSession, Retrofit, Ktor)를 사용하여 API에서 데이터를 가져옵니다. LocalDataSource는 로컬 저장소(CoreData, Realm, Room, UserDefaults, DataStore)로 작업합니다. 각 DataSource는 좁은 책임을 가집니다: RemoteDataSource는 API 요청 형식만 알고, LocalDataSource는 데이터베이스 스키마만 압니다. Repository는 이를 결합하여 캐싱 전략을 구현합니다.
| DataSource | iOS 플랫폼 | Android 플랫폼 | 소스 |
|---|---|---|---|
| Remote | URLSession + Codable | Retrofit + Moshi/Gson | REST / GraphQL API |
| Local (DB) | CoreData, SwiftData | Room, SQLDelight | 기기 내 SQLite |
| Local (캐시) | NSCache, UserDefaults | DataStore, EncryptedSP | 인메모리 / 디스크 |
| 설정 | UserDefaults, Keychain | SharedPreferences, EncryptedSP | 설정, 토큰 |
Repository의 캐싱 전략: Cache-First(먼저 캐시, 그다음 백그라운드 로드), Network-Only(네트워크만, 결제 화면용), Network-First-With-Cache-Backup(먼저 네트워크, 오류 시 캐시로 폴백). 전략 선택은 시나리오에 따라 다릅니다: 국가 목록은 오래 캐시 가능, 환율 — 15분, 지갑 잔액 — 네트워크만. Repository는 전략을 구현하고 ViewModel 또는 UI를 변경하지 않고 전략을 변경합니다.
Repository와 Service는 기능이 중복되는 서로 다른 패턴입니다. Repository는 데이터 액세스 및 캐싱을 담당하며 데이터 모델을 반환합니다. Service(또는 Interactor, Use Case)는 비즈니스 로직을 포함합니다: 유효성 검사, 데이터 변환, 여러 Repository 호출 조정. Service는 주문을 처리하기 위해 UserRepository, OrderRepository 및 NotificationRepository를 결합할 수 있습니다. Repository는 비즈니스 로직을 포함하지 않습니다 — CRUD 및 캐싱만 담당합니다.
Repository를 선택해야 할 때 — 여러 소스(API + DB + 캐시)를 사용한 데이터 탐색, 오프라인 우선 아키텍처, 캐싱 및 투명한 소스 전환 필요. Clean Architecture에서 Repository는 필수이며 Google에서 Android 애플리케이션에 권장합니다. iOS의 VIPER 아키텍처에서 Repository의 역할은 Interactor 계층이 수행하며, 데이터 액세스를 위해 Manager 또는 Service와 상호 작용합니다.
Service로 충분할 때 — 단일 데이터 소스를 가진 간단한 애플리케이션, 쓰기 없는 읽기 전용 화면, 오프라인 모드가 없는 프로젝트. 이러한 경우 DataSource가 ViewModel 또는 Presenter에 의해 직접 사용되며 Repository는 불필요한 계층이 됩니다. 그러나 초기 단계에서 Repository를 추가해도 큰 노력이 필요하지 않으며 향후 캐싱 및 테스트 추가가 간소화됩니다.
자주 묻는 질문
DataSource는 단일 소스(API, DB, 캐시)로 작업하는 클래스입니다. Repository는 여러 DataSource를 관리하고 통합 인터페이스를 제공하는 클래스입니다. Repository는 어떤 DataSource를 사용할지 결정하고 캐싱을 조정합니다. DataSource는 다른 소스의 존재를 알지 못합니다. Repository는 각 소스의 구현 세부 사항을 알지 못합니다.
네, Repository는 SwiftUI에서 데이터를 View로부터 분리하는 데 유용합니다. ViewModel은 Repository의 Publisher를 구독하고, Repository는 캐싱 및 동기화를 관리합니다. 간단한 애플리케이션에서는 ViewModel에서 직접 URLSession을 사용할 수 있지만, 테스트 용이성과 확장성을 위해 Repository가 선호됩니다. Apple은 패턴을 강제하지 않지만 SwiftData 및 Network.framework와 호환됩니다.
DataSource는 의존성 주입을 통해 모의 객체로 대체됩니다. 테스트는 모의 RemoteDataSource(미리 정의된 JSON 반환)와 모의 LocalDataSource(데이터 저장 확인)를 생성합니다. Repository는 격리되어 테스트됩니다: 캐싱 전략, 오류 처리 및 올바른 호출 순서가 검증됩니다. 통합 테스트를 위해 TestDispatcher(Kotlin) 또는 MainActor.run(Swift)이 사용됩니다.
가능하지만 권장되지 않습니다. 프로토콜이 없으면 테스트 및 미리보기에서 구현을 대체할 수 없습니다. Kotlin에서 Repository 인터페이스는 DI(Dagger, Hilt, Koin)를 통한 구현 대체를 허용합니다. Swift에서 async-await 및 Combine 코드 테스트를 위해 Repository 프로토콜이 필수입니다. 예외는 캐싱 로직이 없는 단일 데이터 소스의 간단한 프로젝트입니다.
오프라인 우선은 애플리케이션이 로컬 데이터를 사용하여 인터넷 없이 작동하는 전략입니다. Repository가 핵심 역할을 합니다: 먼저 로컬 DataSource에서 데이터를 반환한 후 백그라운드에서 서버와 동기화합니다. 사용자는 데이터를 즉시 보고 Repository는 네트워크에서 로드한 후 업데이트합니다. Flow를 사용하는 Room은 로컬 데이터베이스의 데이터 변경 시 반응형 UI 업데이트를 제공합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.