はじめに
Javaで業務システムを開発していると、しばしば見かけるのがControllerにビジネスロジックが入り込んでいたり、Repositoryで画面都合の加工をしてしまったりといった「責務の崩壊」です。
短期的には動くものが作れるのですが、長期的には以下の問題を引き起こします。
- 影響範囲が読めず修正に時間がかかる
- 新機能追加時にコピペ実装が増える
- 属人化して他の人が触れない
そこで今回は、三層アーキテクチャ(Controller / Service / Repository)の責務を整理し、
「なぜ分けるべきか」「守らないとどうなるか」 を実例ベースで紹介します。
三層アーキテクチャとは
JavaのWebアプリケーションでよく採用される設計パターンです。
[Controller] → [Service] → [Repository] → DB
各層の役割
-
Controller
- リクエスト/レスポンスの入り口
- 入出力のバリデーション、DTO変換など
- ビジネスロジックは書かない
-
Service
- ビジネスロジックの中心
- 複数Repositoryを組み合わせる処理
- トランザクション制御
-
Repository
- データアクセス層(DBとのやり取り専任)
- SQLやORM(JPA, MyBatisなど)の呼び出し
- ビジネスロジックを持たない
実例で見る「責務崩壊」の問題点
❌ Controllerにビジネスロジックを書いてしまった例
// NG例: Controllerが複雑化している
@PostMapping("/purchase")
public ResponseEntity<String> purchase(@RequestBody PurchaseRequest request) {
if (request.getAmount() <= 0) {
return ResponseEntity.badRequest().body("Invalid amount");
}
User user = userRepository.findById(request.getUserId()).orElseThrow();
if (user.getBalance() < request.getAmount()) {
return ResponseEntity.badRequest().body("Insufficient balance");
}
user.setBalance(user.getBalance() - request.getAmount());
userRepository.save(user);
return ResponseEntity.ok("Purchase success");
}
- Controllerにビジネスルール(残高チェック、残高更新)が入り込んでいる
- テストが困難
- 他のユースケースで「残高チェック」を再利用できない
✅ 責務を分けた例
// Controller: 入出力処理に専念
@PostMapping("/purchase")
public ResponseEntity<String> purchase(@RequestBody PurchaseRequest request) {
purchaseService.purchase(request.getUserId(), request.getAmount());
return ResponseEntity.ok("Purchase success");
}
// Service: ビジネスロジックを記述
@Service
public class PurchaseService {
private final UserRepository userRepository;
public PurchaseService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public void purchase(Long userId, int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Invalid amount");
}
User user = userRepository.findById(userId).orElseThrow();
if (user.getBalance() < amount) {
throw new IllegalStateException("Insufficient balance");
}
user.setBalance(user.getBalance() - amount);
userRepository.save(user);
}
}
// Repository: DBアクセス専任
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
- Controllerがすっきりし、Service層でビジネスルールを集中管理できる。
- 残高チェックロジックを他のユースケースでも再利用可能。
✅よくある責務違反パターンと対策
責務違反パターン | 問題点 | レビュー時の対策 |
---|---|---|
Service層がSQLを直書きしている | DB依存が高まり、テスト困難 | Repository層へ移動し、JPA/JDBC経由で扱う |
Service層が複雑な業務ロジックを持つ | 可読性が低下、テストが複雑化 | Domain/Logic層へロジックを抽出 |
Repository層がビジネスルールを実装している | DB層と業務知識が密結合 | Service/Domain層にルールを移す |
Controllerが業務ロジックを持つ | テストが難しく再利用性が低下 | Service層に処理を委譲する |
共通処理をServiceにベタ書き | 横断的関心事が肥大化 | UtilityクラスやAOPに切り出す |
エラーハンドリングをRepository層で実装 | 層をまたぐ例外設計が不整合 | Service層で統一的にハンドリング |
責務分担を守るメリット
-
可読性向上
- 各層の役割が明確なので、コードを追いやすい
-
テスト容易性
- Service単体のテストがしやすい(MockでRepositoryを差し替え可能)
-
変更に強い
- DBをMyBatisからJPAに変えてもService/Controllerに影響しない
-
保守コスト削減
- 「どこに何があるか」が明確なので、新規参画メンバーもキャッチアップしやすい
まとめ
三層アーキテクチャの責務分担は「古典的」ですが、
長期的な保守・機能追加コストを劇的に下げる 効果があります。
-
Controller → 入出力処理
-
Service → ビジネスロジック
-
Repository → データアクセス
これを徹底するだけで、プロジェクトの見通しは大きく変わります。