0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

三層アーキテクチャ(Controller / Service / Repository)の責務分担

Posted at

はじめに

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 → データアクセス

これを徹底するだけで、プロジェクトの見通しは大きく変わります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?