📑

なぜリポジトリ層にだけインターフェースが必要なのか?

に公開

はじめに

以前受けた研修では以下のような構成でバックエンドサービスを開発しました

  • controller(コントローラー層)← リクエストの受け口を担当
  • service(サービス層)← ビジネスロジックを担当
  • repository(リポジトリ層)← DB接続を担当

TL;DR

  1. テストを楽にするため
  2. ソフトウェアに柔軟性を持たせるため

疑問:なぜリポジトリ層にだけインターフェースが必要なのか?

そもそもインターフェースがないとどうなるのか

サービス層は以下のようなコードになるかと思います。

public class UserService {
  private final UserRepository userRepository;

  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }
  ...
  public User getUser(int id) {
    return userRepository.find(id);
  }
}

// テストコード
@Test
public void testGetUser() {
  UserRepository repo = new UserRepository();
  UserService service = new UserService(repo);
  
  User user = service.getUser(1); // DBへ接続
  assertEquals("hoge", user.getName());
}

サービス層では重要なビジネスロジックを扱っています。
そのため、サービス層でのテストの実施は不可欠です。

では、上記のようなコードをテストするとどうなるでしょうか?
ユーザ取得のテストは以下のようになるかと思います。

  1. UserService 内の getUser メソッドが呼び出される
  2. UserRepository 内の find メソッドが呼び出される
  3. (書いていませんが)find メソッドを実行したことで DB にユーザ情報を取得しにいく
  4. UserService までユーザの情報が返ってくる

実は、上記のフローには DBに接続する という問題点があります。

なぜ DB に接続することが問題なのか

DB に接続するというよりも、単体テストにおいては DB 含め、外部サービスに接続することが問題となります。

  1. ロジックのテストをしたいのに、外部サービスが利用できなくなったらテストが通らなくなり、テストの信頼性が低下する。
  2. 上記に加え、外部サービス との接続試験のようになってしまい、目的が曖昧になる。
  3. DB や外部 API の場合、I / O 処理などに時間がかかる。
  4. 外部 API の場合、API の利用回数などで課金される可能性もある。
  5. テスト内容によっては、DB の中身が同一でないとテストが通らないケースが出てきてしまい、チームメンバー全員で DB の内容を一致させる必要がでてきてしまう。

ではどうするのか?

以下のようにインターフェースを用意し、モックを使います。

// プロダクションコード
public class UserService {
  private final UserRepositoryInterface userRepository;

  public UserService(UserRepositoryInterface userRepository) {
    this.userRepository = userRepository;
  }
  
  public User getUser(int id) {
    return userRepository.find(id);
  }
}

// テストコード
@Test
public void testGetUser() {
  // 決まった値(id: 1, name: "hoge")を返すだけのモック
  UserRepositoryInterface mockRepo = new UserRepositoryMock();
  UserService service = new UserService(mockRepo);
  
  User user = service.getUser(1); // DB接続不要
  assertEquals("hoge", user.getName());
}

このようにすれば外部接続が必要なく、高速かつ効率的にテストを行うことができます。

また、インスタンス変数の型をインターフェースにしたため、インターフェースを実装した具象クラスなら全て代入可能となるので、柔軟性も高くなります。

補足:なぜコントローラ層やサービス層にはインターフェースがないのか?

この文脈においてインターフェースが必要になるのは、以下の条件を両方とも満たす場合です:

  1. そのクラスに依存している他のクラスがあり、その依存元をテストしたい
  2. そのクラスが外部接続(DB、API等)を行う

リポジトリの場合(両方満たす)

✅ サービス層が依存している(サービス層をテストしたい)
✅ DBに接続する
→ インターフェースが必要

コントローラ層の場合

❌ コントローラ層は他の層から依存されることが少ない
❌ 外部接続しない
→ インターフェース不要

サービス層の場合

❌ コントローラ層が依存しているが、コントローラ層はテストするほどの重要なロジックを持たない
❌ サービス層自体は外部接続を行わない(リポジトリのインターフェース経由でアクセスするため)
→ インターフェース不要

Discussion