📑
なぜリポジトリ層にだけインターフェースが必要なのか?
はじめに
以前受けた研修では以下のような構成でバックエンドサービスを開発しました
- controller(コントローラー層)← リクエストの受け口を担当
- service(サービス層)← ビジネスロジックを担当
- repository(リポジトリ層)← DB接続を担当
TL;DR
- テストを楽にするため
- ソフトウェアに柔軟性を持たせるため
疑問:なぜリポジトリ層にだけインターフェースが必要なのか?
そもそもインターフェースがないとどうなるのか
サービス層は以下のようなコードになるかと思います。
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());
}
サービス層では重要なビジネスロジックを扱っています。
そのため、サービス層でのテストの実施は不可欠です。
では、上記のようなコードをテストするとどうなるでしょうか?
ユーザ取得のテストは以下のようになるかと思います。
-
UserService内のgetUserメソッドが呼び出される -
UserRepository内のfindメソッドが呼び出される - (書いていませんが)
findメソッドを実行したことで DB にユーザ情報を取得しにいく -
UserServiceまでユーザの情報が返ってくる
実は、上記のフローには DBに接続する という問題点があります。
なぜ DB に接続することが問題なのか
DB に接続するというよりも、単体テストにおいては DB 含め、外部サービスに接続することが問題となります。
- ロジックのテストをしたいのに、外部サービスが利用できなくなったらテストが通らなくなり、テストの信頼性が低下する。
- 上記に加え、外部サービス との接続試験のようになってしまい、目的が曖昧になる。
- DB や外部 API の場合、I / O 処理などに時間がかかる。
- 外部 API の場合、API の利用回数などで課金される可能性もある。
- テスト内容によっては、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());
}
このようにすれば外部接続が必要なく、高速かつ効率的にテストを行うことができます。
また、インスタンス変数の型をインターフェースにしたため、インターフェースを実装した具象クラスなら全て代入可能となるので、柔軟性も高くなります。
補足:なぜコントローラ層やサービス層にはインターフェースがないのか?
この文脈においてインターフェースが必要になるのは、以下の条件を両方とも満たす場合です:
- そのクラスに依存している他のクラスがあり、その依存元をテストしたい
- そのクラスが外部接続(DB、API等)を行う
リポジトリの場合(両方満たす)
✅ サービス層が依存している(サービス層をテストしたい)
✅ DBに接続する
→ インターフェースが必要
コントローラ層の場合
❌ コントローラ層は他の層から依存されることが少ない
❌ 外部接続しない
→ インターフェース不要
サービス層の場合
❌ コントローラ層が依存しているが、コントローラ層はテストするほどの重要なロジックを持たない
❌ サービス層自体は外部接続を行わない(リポジトリのインターフェース経由でアクセスするため)
→ インターフェース不要
Discussion