Spring BootのServiceに業務ロジックを書く理由を解説する記事のアイキャッチ。若手エンジニアがJava道場風の学習空間でServiceの責務を学ぶイラスト。

Spring

Spring BootのServiceに業務ロジックを書く理由

AIによる要約

Spring BootのServiceは、業務処理やユースケースを表す中心的な場所です。Controllerに業務ロジックを書くと入口ごとに処理が散らばります。Serviceに寄せることで、再利用しやすく、テストしやすく、トランザクション境界も考えやすくなります。

新人SE
新人SE
Serviceって、ControllerからRepositoryを呼ぶだけの中継役なんですか?
ポンコツSE
ポンコツSE
単なる中継役ではありません。業務ルール、処理の順番、Repositoryの呼び出し、更新単位をまとめる場所です。

Spring Bootの現場でよく言われるのが「業務ロジックはServiceに書きましょう」という話です。

ただ、初学者にとっては、Controllerに書いても動くのに、なぜServiceへ移す必要があるのか分かりにくいかもしれません。

この記事のポイント

  • Serviceはユースケースや業務処理を表す場所
  • Controllerは入口、RepositoryはDBアクセスに集中する
  • Serviceに寄せると再利用とテストがしやすい
  • Serviceが大きくなりすぎる場合は分割のサイン

Serviceは業務処理の置き場所

Serviceには、業務上の判断や処理の順番を書きます。たとえば、注文キャンセル、ユーザー登録、在庫引当、請求作成のように、アプリケーションのユースケースを表す処理です。

@Service
public class OrderService {
    private final OrderRepository orderRepository;

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

    public void cancel(Long orderId) {
        Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new OrderNotFoundException(orderId));

        order.cancel();
        orderRepository.save(order);
    }
}

このコードでは、注文を取得し、キャンセルし、保存するという処理の流れがServiceにまとまっています。ControllerはこのServiceを呼ぶだけで済みます。

Controllerに書くと入口ごとに散らばる

業務ロジックをControllerに書くと、同じ処理を別の入口から使いたいときに困ります。画面、API、バッチ、管理画面で似た処理が必要になることは珍しくありません。

@PostMapping("/orders/{id}/cancel")
public ResponseEntity<Void> cancel(@PathVariable Long id) {
    Order order = orderRepository.findById(id).orElseThrow();
    if (!order.canCancel()) {
        throw new IllegalStateException("キャンセルできません");
    }
    order.cancel();
    orderRepository.save(order);
    return ResponseEntity.ok().build();
}

この処理がControllerにあると、バッチからキャンセル処理を使いたいときに再利用しづらくなります。Serviceにあれば、別の入口からも同じ業務ルールを呼べます。

Serviceに書くとテストしやすい

ControllerはHTTPリクエストやレスポンスに関係します。一方、Serviceは業務処理に集中できるため、テストで確認したいことが明確になります。

@Test
void 出荷済み注文はキャンセルできない() {
    Order order = Order.shipped(1L);
    when(orderRepository.findById(1L)).thenReturn(Optional.of(order));

    assertThrows(CannotCancelOrderException.class, () -> {
        orderService.cancel(1L);
    });
}

このテストでは、HTTPステータスやURLではなく、出荷済み注文をキャンセルできないという業務ルールだけを確認できます。

RepositoryはDBアクセスに集中する

Repositoryに業務ロジックを書きすぎるのも避けたいです。Repositoryは、DBから取得する、保存する、検索する、といった永続化の責務に寄せます。

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByCustomerId(Long customerId);
}

キャンセルできるかどうか、通知を送るかどうか、在庫を戻すかどうかは、RepositoryではなくService側で組み立てる方が読みやすくなります。

Serviceは何でも入れる場所ではない

Serviceが大事だからといって、すべてを1つのServiceに詰め込むのも問題です。UserServiceに登録、更新、退会、権限、通知、CSV出力まで全部入ると、結局読みづらくなります。

Serviceが大きくなりすぎたサイン

  • 1つのServiceに無関係なユースケースが増えている
  • メソッド名だけでは何をするか分からない
  • privateメソッドが増えすぎて処理の流れを追いにくい
  • テスト対象が広すぎて準備が大変
  • Controllerごとに似たServiceメソッドが増えている

この場合は、ユースケースごとにServiceを分ける、処理の一部を別クラスに切り出す、MapperやValidatorを分けるなどを検討します。

DIとServiceの関係

Springでは、ServiceをnewするのではなくDIで受け取ることが多いです。これは、Repositoryや外部連携クラスを差し替えやすくし、テストしやすくするためです。

@Service
public class UserService {
    private final UserRepository userRepository;
    private final MailService mailService;

    public UserService(UserRepository userRepository, MailService mailService) {
        this.userRepository = userRepository;
        this.mailService = mailService;
    }
}

Serviceは業務処理の中心になるため、必要な部品をDIで受け取り、処理の流れを組み立てます。

現場レビューではどう見られるか

// レビューコメント例
// Controllerにユーザー登録時の重複チェックとメール送信処理が入っています。
// 登録処理は画面以外からも呼ばれる可能性があるため、
// UserService.register に寄せてください。

このコメントは、単にレイヤーをきれいにしたいだけではありません。業務ルールを1か所に集め、入口が増えても同じ処理を使えるようにするための指摘です。

レビュー前のセルフチェック

Serviceを書く前に見ること

  • その処理は業務ルールか、HTTPの入口処理か
  • 同じ処理を別のControllerやバッチから使う可能性がないか
  • Repositoryに業務判断を書いていないか
  • Serviceメソッド名がユースケースを表しているか
  • Serviceが大きくなりすぎていないか

この記事とあわせて読みたい

まとめ

Serviceは、ControllerとRepositoryの中継役ではなく、業務処理を表す場所です。業務ロジックをServiceに寄せると、入口が増えても再利用しやすく、テストしやすく、変更にも強いコードになります。

-Spring
-, , , ,