Controllerがゴチャつく前に!Service層でスッキリ整理する方法【Spring Boot】

  • LINEで送る
Controllerがゴチャつく前に!Service層でスッキリ整理する方法

 Spring Bootで最初のAPIを作り、@RestControllerに慣れてきたあなた。ついつい勢いでロジックを詰め込んでいませんか?最初は成長実感があって楽しい反面、やがてこんな悩みに直面するはずです。

「ロジックが増えて、Controllerが見づらい…、テストが大変で、変更のたびに壊れそう…」

 あなたのコード、「Controller肥大化」に陥っているかも。この記事では、なぜService層が必要なのか、どうやったら、保守のしやすいコードになるかを、実際のコードを踏まえて解説していきます。

これを読めばあなたのコードが格段と良くなる!それでは初めていきましょう。

🎯 Service層って何?まずはイメージで理解!

 Spring Bootでは、アプリケーションの処理を「Controller」「Service」「Repository」の3つの層に分けて考えるのが一般的です。これを三層アーキテクチャと呼びます。

三層アーキテクチャ

各層の役割

  • Controller(プレゼンテーション層)
    ユーザーからの入力やリクエストを受け取り、結果を返す役目。
  • Service(アプリケーション層)
    実際のビジネス処理を担当。ここでロジックを整理し、他の層と協力して仕事を進めます。
  • Repository(データ層)
    データベースへの読込・書込処理を専門に行います。

この中で、Service層はアプリケーションの処理ロジックを整理するための中心的な存在です。

💡 補足:三層アーキテクチャやクライアント・サーバモデルの基本構造については、下記の記事でも別の視点から紹介しています。あわせて読むと理解が深まります。

⚙️「ロジックはここへ!」Service層導入の設計感覚

 Service層の正体についてはわかったはずです。ではなぜControllerにロジックを書かない方がいいのでしょうか?

 Controllerはあくまで「リクエストを受け取り、サービスに渡す窓口にしたほうがいいんです。。ビジネスロジックがControllerに混在すると:

  • コードが長くなり、読みづらくなる
  • テストがしづらくなる(HTTPテストかロジック単体かの切り分けが難しい)
  • 複数のControllerに同じロジックをコピー&ペーストしがちで、修正が大変になる

 こうした問題を避けるため、アプリケーションの根幹となる処理はService層に任せるのが有効です。

具体例:ロジックをServiceに移したときの違い

❌ Controllerにロジックを書いた例

@RestController
public class OrderController {
    private final OrderRepository orderRepo;
    // 注文情報を取得するための準備
    public OrderController(OrderRepository orderRepo) {
        this.orderRepo = orderRepo;
    }
  
    @GetMapping("/orders/{id}")
    public OrderDto getOrder(@PathVariable Long id) {
        // 注文情報の取得
        Order order = orderRepo.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("注文が見つかりません"));
        // 注文情報のチェック
        if (!order.getStatus().equals("ACTIVE")) {
            throw new IllegalStateException("注文が無効です");
        }
        // 注文情報の変換・表示
        return new OrderDto(order.getId(), order.getTotal());
    }
}
  • Controllerがデータ取得・チェック・変換、表示まで全部やっている
  • テストではMockMvc(Contorollerをテストするライブラリ)を使って、DBモックまで用意しなければならず、煩雑です。

✅ Serviceに処理を移した例

@RestController
public class OrderController {
    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/orders/{id}")
    public OrderDto getOrder(@PathVariable Long id) {
        // サービスから取得した注文情報を返却するだけ
        return orderService.getActiveOrder(id);
    }
}
@Service
public class OrderService {
    private final OrderRepository orderRepo;

    public OrderService(OrderRepository orderRepo) {
        this.orderRepo = orderRepo;
    }

    public OrderDto getActiveOrder(Long id) {
        // 注文情報の取得
        Order order = orderRepo.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("注文が見つかりません"));
        // 注文情報のチェック
        if (!"ACTIVE".equals(order.getStatus())) {
            throw new IllegalStateException("注文が無効です");
        }
        // 注文情報のオブジェクトを変換して返却
        return new OrderDto(order.getId(), order.getTotal());
    }
}
  • Controllerは単純な呼び出しだけに
    • 見やすく保守しやすい
  • Serviceにビジネス処理を集中させる
    • テスト・再利用しやすく
  • トランザクション処理もこの層で管理しやすくなる

 上記のようにContorollerにはリクエストを受け付けて、結果を表示するだけのシンプルな構造にすることができました。それぞれの層での責務を明確にすることで、保守性が向上したり、テストがはるかに簡単になったりします。

「まずは簡単に触ってみよう」Serviceクラスの作り方

Step 1:Serviceクラスを作ってみよう

 Spring Bootでは、ビジネス処理を担当するクラスに @Service アノテーションを付けます。
これでSpringがこのクラスを管理する「Bean」として使えるようになります。

@Service
public class HelloService {
    public String getHelloMessage() {
        return "Hello, Spring Boot!";
    }
}

💡 Beanとは、SpringのIoCコンテナが生成・管理するオブジェクトのことです。newによる手動生成ではなく、Springがインスタンス化や依存注入を管理してくれます

✅ Step 2:Controllerから呼び出す準備

 次に、HelloControllerHelloService を使います。ここで「依存性注入(DI)」という仕組みを利用し、Serviceを注入します。

@RestController
public class HelloController {
    private final HelloService helloService;

    // Serviceクラスは自動的に注入される
    public HelloController(HelloService helloService) {
        this.helloService = helloService;
    }

    @GetMapping("/hello")
    public String sayHello() {
        return helloService.getHelloMessage();
    }
}

 このように、@Service をつけたクラスはSpring管理のBeanになり、Contorollerのコンストラクタ引数として受け取ることが可能になります。引数にはSpringが自動でインスタンスを「注入」してくれます。

 Beanに登録される名前はクラス名の先頭を小文字にした名前(例:HelloServicehelloService)で登録されます。名前を変えたい場合は、@Service("customName") のように明示できます。

💡 DIについてちょっと補足

依存性注入(DI)は、クラス同士を直接 newせずに接続する方法で、以下のメリットがあります:

  • テストがしやすい:Serviceを新しいControllerに簡単に差し替えできるため
  • 依存構造が明確:コンストラクタを見るだけで何が必要か分かる
  • 処理が安定:必要なBeanが揃っていないと起動時にエラーになることで安全性向上

 ただし、今回はServiceクラスを作って動かす流れに集中するのが目的なので、詳しいDIの解説は別の記事で扱います。ここでは「DIの仕組みがあってServiceが注入されるんだな」という理解でOKです。

まとめ

 本記事では、Spring Bootにおける三層アーキテクチャの考え方を踏まえ、Controllerの肥大化を防ぐためにService層を導入する方法を紹介しました。ロジックをControllerから切り離してServiceに任せることで、可読性・保守性の高い構造を実現できます

 また、@ServiceアノテーションでクラスをBeanとして登録し、DI(依存性注入)によってControllerから自然に呼び出す仕組みについても軽く触れました。こうした構成を理解しておくことで、今後の開発において「処理をどこに書くべきか」を判断できる設計力が身につきます。

 次回は、今回作成したService層を活かしながら、HTMLテンプレートエンジン「Thymeleaf」を使って、データを画面に出力する方法を学んでいきます。

最新の投稿

SNSでもご購読できます。

コメント

コメントを残す