Repository Pattern: その概要、iOSとAndroidにおけるデータ抽象化パターン

著者: IT Sectr 公開日: 2026-02-17 読了時間: 7 分

Repository Pattern — ビジネスロジックとデータソースの間に抽象化レイヤーを追加するパターンです。API、データベース、キャッシュを直接呼び出す代わりに、Repositoryはデータの取得と保存のための統一インターフェースを提供します。これにより、テストとソースの切り替えが簡素化されます。詳細はAndroid Data Layerのドキュメントをご覧ください。

重要なポイント

  • Repository Pattern — ビジネスロジックとデータソース(API、DB、キャッシュ)の中間レイヤー
  • DataSource — 各ソースの個別クラス: RemoteDataSource、LocalDataSource
  • 単一の真実源 — RepositoryがUIレイヤーの唯一のデータソースになる
  • テスト — DIを介してRepositoryをモックオブジェクトに簡単に置き換え可能(単体テスト用)
  • 互換性 — MVVM、Clean Architecture、その他のアーキテクチャパターンと連携

モバイル開発におけるRepository Patternとは?

Repository Patternは、ビジネスロジックをデータソースへの直接アクセスから隔離する構造パターンです。Activity、UIViewController、ViewModelが直接Retrofit、URLSession、Room、CoreDataを呼び出す代わりに、Repositoryと通信します。Repositoryはデータをネットワーク、データベース、キャッシュのどこから取得するかを決定し、統一された形式で結果を返します。これは単一責任の原則を実装しています — UIはデータがどのように、どこから取得されたかを知りません。

Repositoryのコンポーネントには、インターフェース(プロトコル)、実装、1つ以上のDataSourceが含まれます。DataSourceは単一のソースを扱うクラスです: RemoteDataSourceはHTTPクライアントを介してAPIを呼び出し、LocalDataSourceはデータベースの読み書きを行います。Repositoryはコンストラクタ(依存性注入)を介してDataSourceを受け取り、使用するソースを決定します。たとえば、ユーザー一覧を要求する場合、Repositoryは最初にキャッシュ、次にデータベース、次にネットワークを確認します。

Repository Patternの利点: データソースの変更(API変更、DB移行)がUIレイヤーに影響しないこと、RepositoryまたはDataSourceの置き換えによる単体テスト、UIに対して透過的なキャッシュ、画面ロジックを変更せずにオンラインとオフラインモードを切り替え可能。AndroidコミュニティはClean ArchitectureにおいてRepositoryを必須レイヤーとして推奨しています。

Swiftを使用したiOSでのRepository Pattern: 実装と例

iOSの実装におけるRepositoryはSwiftのプロトコルに基づいています。Repositoryプロトコルはデータの取得と保存のメソッドを宣言します。実際の実装はイニシャライザを介して注入されます — これにより、テストやSwiftUIプレビューでの実装の置き換えが可能になります。DataSourceもプロトコルとして宣言されます: Protocol RemoteDataSource、Protocol LocalDataSource。ViewModelやInteractorは特定の実装を知りません — Repositoryプロトコルのみを知っています。

swift
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を返します。

Kotlinを使用したAndroidでのRepository Pattern: Flowの例

Androidの実装におけるRepositoryは、非同期操作にKotlin CoroutinesとFlowを広く利用しています。Googleは公式のAndroidアーキテクチャガイド(Android Architecture Components)でRepositoryを推奨しています。Repositoryはコンストラクタを介してRemoteDataSource(Retrofit)とLocalDataSource(Room)を受け取り、ViewModelはRepositoryからのFlowを購読します。Repositoryはデータ戦略を管理します: キャッシュファースト、ネットワークファースト、または常にネットワークでキャッシュへの書き込み。

kotlin
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の標準です: シールドクラスResultがViewModelにローディング状態(Loading、Success、Error)を通知します。ViewModelはcollectを介して購読し、StateFlowまたはLiveDataを更新します。Flowを使用したRepositoryはデータベースの変更をUIに自動的に通知します — これは手動更新なしではUIが変更を認識できない単発リクエストとの重要な違いです。

DataSource: Remote、Local、データキャッシング

DataSource — 特定のデータソースを扱うクラスです。RemoteDataSourceはHTTPクライアント(URLSession、Retrofit、Ktor)を使用してAPIからデータを取得します。LocalDataSourceはローカルストレージ(CoreData、Realm、Room、UserDefaults、DataStore)を扱います。各DataSourceは狭い責任を持ちます: RemoteDataSourceはAPIリクエスト形式のみを知り、LocalDataSourceはデータベーススキーマのみを知ります。Repositoryはこれらを組み合わせ、キャッシュ戦略を実装します。

DataSourceiOSプラットフォームAndroidプラットフォームソース
RemoteURLSession + CodableRetrofit + Moshi/GsonREST / GraphQL API
Local (DB)CoreData, SwiftDataRoom, SQLDelightデバイス上のSQLite
Local (キャッシュ)NSCache, UserDefaultsDataStore, EncryptedSPインメモリ / ディスク
設定UserDefaults, KeychainSharedPreferences, EncryptedSP設定、トークン

Repositoryのキャッシュ戦略: Cache-First(最初にキャッシュ、次にバックグラウンドロード)、Network-Only(ネットワークのみ、支払い画面用)、Network-First-With-Cache-Backup(最初にネットワーク、エラー時にキャッシュにフォールバック)。戦略の選択はシナリオに依存します: 国のリストは長期間キャッシュ可能、為替レートは15分間、ウォレット残高はネットワークのみ。Repositoryは戦略を実装し、ViewModelやUIを変更せずにそれを変更します。

Repository Pattern vs Service Layer: 違いと選択

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を追加しても大きな労力は必要なく、将来的なキャッシュ追加やテストが容易になります。

よくある質問

RepositoryとDataSourceの違いは?

DataSourceは単一のソース(API、DB、キャッシュ)を扱うクラスです。Repositoryは複数のDataSourceを管理し、統一インターフェースを提供するクラスです。RepositoryはどのDataSourceを使用するかを決定し、キャッシュを調整します。DataSourceは他のソースの存在を知りません。Repositoryは各ソースの実装詳細を知りません。

SwiftUIを使用したiOSにRepositoryは必要ですか?

はい、RepositoryはSwiftUIにおいてデータをViewから分離するために有用です。ViewModelはRepositoryからのPublisherを購読し、Repositoryはキャッシュと同期を管理します。シンプルなアプリケーションではViewModel内で直接URLSessionを使用できますが、テスト容易性とスケーラビリティの点でRepositoryが推奨されます。Appleはパターンを強制していませんが、SwiftDataやNetwork.frameworkとの互換性があります。

複数のDataSourceを持つRepositoryをテストするには?

DataSourceは依存性注入を介してモックオブジェクトに置き換えられます。テストはモックRemoteDataSource(事前定義されたJSONを返す)とモックLocalDataSource(データが保存されたことを確認)を作成します。Repositoryは分離してテストされます: キャッシュ戦略、エラー処理、正しい呼び出し順序が検証されます。統合テストにはTestDispatcher(Kotlin)またはMainActor.run(Swift)が使用されます。

インターフェース(プロトコル)なしでRepositoryを使用できますか?

可能ですが推奨されません。プロトコルがないと、テストやプレビューでの実装の置き換えが不可能です。Kotlinでは、RepositoryインターフェースによりDI(Dagger、Hilt、Koin)を介した実装の置き換えが可能です。Swiftでは、async-awaitおよびCombineコードのテストにRepositoryプロトコルが必須です。例外は、キャッシュロジックのない単一データソースのシンプルなプロジェクトです。

Repositoryの文脈でのoffline-firstとは?

Offline-firstは、アプリケーションがローカルデータを使用してインターネットなしで動作する戦略です。Repositoryが重要な役割を果たします: 最初にローカルDataSourceからデータを返し、その後バックグラウンドでサーバーと同期します。ユーザーはデータを即座に表示し、Repositoryはネットワークからの読み込み後にデータを更新します。Flowを使用したRoomは、ローカルデータベースのデータ変更時にリアクティブなUI更新を提供します。

まとめ

  • Repository Pattern — UIとデータソース間の抽象化レイヤー
  • DataSource — API、DB、キャッシュ用の個別クラス
  • プロトコル — テストと実装の置き換えに必須
  • キャッシュ戦略 — Cache-First、Network-Only、Network-First-With-Cache-Backup
  • iOS — プロトコルを用いたasync-awaitまたはCombine
  • Android — Kotlin Flow + Room + Retrofit、Google推奨アプローチ
  • テスト — DIによるモックDataSource、キャッシュ戦略の検証

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください