目次
1. はじめに
2. 前回の復習
2.1. Spring Bootは「設備の整ったキッチン」
2.2. 調理器具の役割を知る
3. なぜクラスを分ける必要があるの?
3.1. レストランが人気店になると…
3.2. 以前のJavaでは一つのクラスに何でも書くことがあった
3.3. 「神クラス(God Class)」はどうやって生まれる?
3.4. 「神クラス(God Class)」は何が問題?
4. Spring Bootの設計思想
4.1. Controllerについて
4.2. Serviceについて
4.3. Repositoryについて
5. まとめ
6. おわりに
1. はじめに
この投稿は、「過去の自分がどう教えてほしかったかを思い出しながら、あの頃の自分に向けて説明する」をテーマにしています。
投稿のコンセプトについては、こちらをご覧ください。
2. 前回の復習
2.1. Spring Bootは「設備の整ったキッチン」
前回の記事では、Spring Bootが必要な理由を「設備の整ったキッチンがあると、すぐ料理に取り掛かれる」という例えで説明しました。
もちろん、設備の整ったキッチンがなくても料理を作ることはできます。
しかし、その場合はまずキッチンそのものを用意しなければならず、料理を始めるまでに時間がかかります。
「本当は今すぐ料理を作りたいのに、キッチンから用意しないといけないのか……」という状況です。
同じように、Spring BootがなくてもWebアプリは作れます。
しかし、Webアプリを開発するために必要な共通機能を毎回自分で用意するのは大変です。
開発者が本当に作りたいものは、ECサイトかもしれませんし、顧客管理システムかもしれません。
しかし、そのためには、「サーバーを立ち上げる」「URLを登録する」といった、Webアプリに欠かせないものの、作りたいサービスそのものとは直接関係のない機能まで、一から実装しなければなりません。
Spring Bootは、そうした共通機能をあらかじめ用意してくれるフレームワークです。
そのおかげで、開発者は本来作りたい機能の開発に集中できるようになります。
2.2. 調理器具の役割を知る
しかし、設備が整っていても、それぞれの調理器具の役割や使い方を知らなければ、そのキッチンを十分に活かすことはできません。
Spring Bootも同じです。
便利な仕組みが用意されていても、その役割や使い方を知らなければ、その恩恵を十分に受けることはできません。
また、Spring Bootでは処理を複数のクラスに分けて実装するため、最初は「どのファイルを見ればいいのか分からない」「かえって複雑になった」と感じる人もいるでしょう。
そこで今回は、Spring Bootを使いこなすための第一歩として、Spring Bootでよく使われる「3層アーキテクチャ」を紹介します。
「Controller」「Service」「Repository」がそれぞれどのような役割を持ち、なぜ役割を分けているのかを解説します。
まずは、レストランに登場する3人のスタッフを例に、それぞれの役割を見ていきましょう。
3. なぜクラスを分ける必要があるの?
前回の記事では、Spring BootがWebアプリ開発の土台を用意してくれることを説明しました。
では、その土台の上では、どのようにクラス※1を整理して開発するのでしょうか。
※1
クラスとは、データや処理をひとまとめにした設計図です。
Javaでは、複数のクラスを組み合わせて1つのシステムを作ります。
Spring Bootでは、役割ごとにクラスを分ける「3層アーキテクチャ」という設計がよく使われます。
3.1. レストランが人気店になると…
以前は、お客さんが注文し、シェフが料理を作り、食材庫から食材を取り出して料理を提供するという流れで、Webアプリケーションの仕組みを見ました。
しかし、実際の人気レストランでは、シェフ一人がすべてを担当しているわけではありません。
お店が小さい頃は、店長一人で仕事をこなしていました。
-
お客さんから注文を聞く
-
料理を作る
-
食材を取りに行く
-
会計をする
しかし人気店になると、一人では仕事が回りません。
そこで、それぞれの役割を専門の人に任せることにしました。
-
ウェイター:注文を受け付ける
-
シェフ: 料理を作る
-
倉庫担当 :食材を管理する
すると、それぞれが自分の仕事に集中できるようになりました。
さらに、仕入れ先が変わっても倉庫担当だけが対応すれば済みます。
新メニューを追加するときも、シェフが調理方法を考えればよく、ウェイターの仕事は変わりません。
役割を分けたことで、お店全体が変更に強くなったのです。
Spring Bootも、この考え方を取り入れています。
3.2. 以前のJavaでは一つのクラスに何でも書くことがあった
以前の小規模なJavaアプリケーションや、設計が十分に整理されていないプロジェクトでは、一つのクラスに多くの処理をまとめて書くことがありました。
レストランの例でいうと、店長一人でたくさんの仕事を担当している状態です。
最初は機能が少ないため、一つのクラスに処理をまとめても大きな問題にはなりません。
しかし、機能追加を繰り返すうちに、さまざまな処理が同じクラスに追加されていきます。
すると、一つのクラスが多くの責任を抱え込んだ状態になります。このようなクラスは「神クラス(God Class)」と呼ばれます。
3.3. 「神クラス(God Class)」はどうやって生まれる?
神クラスが生まれるまでの流れを、レストランに注文システムを導入した例で見てみましょう。
各テーブルにタブレットがあります。
お客様は、タブレットのメニューから、料理を選び、注文ボタンを押します。
すると、タブレットから注文システムにアクセスし、厨房へ通知が行きます。
最初は小さなお店でした。
お客様も少ないので、注文システムはこんなクラスでした。
public class OrderManager {
// 注文処理
public Order createOrder(...) { ... }
// メニューを取得する
private Menu getMenu(...) { ... }
// 在庫を確認する
private void checkStock(...) { ... }
// 合計金額を計算する
private int calculateTotalPrice(...) { ... }
// 注文を保存する
private Order saveOrder(...) { ... }
}
※このコード例は、Javaの文法を学ぶためのものではありません。「一つのクラスに処理が集中すると、どうな>るのか」をイメージしてもらうために、処理の詳細は省略しています。
注文システムには、「メニュー取得」「在庫確認」「金額計算」「注文保存」といった処理があり、最後に注文内容を返します。
最初は注文を受けるだけだったため、このクラスでも十分でした。
やがて、SNSで口コミが増え、レストランが人気店になりました。
すると要望が増えます。
-
クーポン対応
-
アレルギー表示
-
ポイント付与
-
ログ保存
-
メール送信
-
注文履歴
-
売上集計
これらをすべて追加すると……
public class OrderManager {
// 注文処理
public Order createOrder(...) { ... }
// メニューを取得する
private Menu getMenu(...) { ... }
// 在庫を確認する
private void checkStock(...) { ... }
// アレルギー情報を表示する
private void showAllergyInfo(...) { ... }
// クーポンを適用する
private void applyCoupon(...) { ... }
// 合計金額を計算する
private int calculateTotalPrice(...) { ... }
// 注文を保存する
private Order saveOrder(...) { ... }
// ポイントを付与する
private void addPoint(...) { ... }
// 注文履歴を保存する
private void saveOrderHistory(...) { ... }
// 売上を更新する
private void updateSales(...) { ... }
// 注文完了メールを送信する
private void sendMail(...) { ... }
// ログを出力する
private void outputLog(...) { ... }
// 厨房へ注文を通知する
private void notifyKitchen(...) { ... }
}
「あれ?注文システムなのに何でもやってるぞ??」
これが、神クラスと呼ばれる状態です。
実際のシステムでは、何百行、何千行ものクラスになります。
3.4. 「神クラス(God Class)」は何が問題?
このままでも動きます。
でも、機能が増えると1つのクラスが何百行、何千行にもなり、修正やテストが大変になります。
①コード量が増え、目的の処理を探しにくくなる
最初は200行だったクラスが、機能追加を繰り返して1000行、2000行……となるケースも少なくありません。
すると、「ポイント付与はどこ?」「メール送信は?」と探すだけでも時間がかかるようになります。
②一つの変更が、別の機能に影響する可能性がある
例えば、メール送信だけ直したつもりなのに、誤って注文保存の処理を触ってしまうかもしれません。
あるいは、売上集計を修正したら、ポイント付与まで影響してしまうかもしれません。
一つの変更が、関係ない機能にも影響する可能性が高くなります。
③複数人で開発すると、同じクラスを編集することが増える
例えば、Aさんはクーポン機能、Bさんはメール送信を修正するとします。
どちらも同じクラスを編集するため、変更内容が競合しやすくなります。Excelで同じファイルを同時編集すると競合するのと同じイメージです。
④テストが難しくなる
たくさんの役割が1つにまとまっているため、関係のない部品まで連動して動いてしまいます。
そのため、テストを1回動かすだけでも、複雑な準備や設定が大量に必要になります。
さらに、あちこちのデータが同時に書き換わるため、どこでエラーが起きたのか原因を見つけるのも大変です。
だからこそ、Spring Bootでは役割ごとにクラスを分ける設計が採用されているのです。
4. Spring Bootの設計思想
4.1. Controllerについて
まずは、レストランの例えを思い出しましょう。
Controllerは、レストランでいうウェイターの役割です。
お客様から注文を受け取り、シェフへ調理を依頼して、最後に料理をお客様のもとへ運びます。
Spring Bootでも同じように、ブラウザから送られてきたHTTPリクエストを受け取り、Serviceへ処理を依頼して、最後にHTTPレスポンスを返します。
// ControllerであることをSpring Bootに伝える
@RestController
public class UserController {
// Serviceクラスを利用する
private final UserService userService;
// コンストラクタでServiceを受け取る
public UserController(UserService userService) {
this.userService = userService;
}
// 「/users」にGETリクエストが送られたときに呼ばれる
@GetMapping("/users")
public List<User> getUsers() {
// Serviceへ処理を依頼し、その結果を返す
return userService.getUsers();
}
}
このコードで注目してほしいのは、Controller自身は何も処理していないことです。
userService.getUsers()を呼び出しているだけですね。
つまり、Controllerは「処理を実行する場所」ではなく、「処理を適切な場所へ振り分ける窓口」の役割をしています。
Webアプリの仕組みについて解説している記事では、ネットワークを「ウェイター」と例えましたが、Spring Bootでは、Controllerがウェイター、ネットワークは「注文を運ぶ通路」に近いです。
4.2. Serviceについて
Serviceは、レストランでいうシェフです。
ホールスタッフから注文を受け取り、料理を作ります。
Spring Bootでは、業務ロジックを書く場所になります。
必要に応じてRepositoryを呼び出し、業務の手順に沿って処理を組み立てます。
// ServiceであることをSpring Bootに伝える
@Service
public class UserService {
// Repositoryクラスを利用する
private final UserRepository userRepository;
// コンストラクタでRepositoryを受け取る
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> getUsers() {
// Repositoryへデータ取得を依頼する
return userRepository.findAll();
}
}
今回の例ではRepositoryへ処理を依頼しているだけですが、必要であれば、割引計算や在庫チェック、会員ランクによる処理の分岐などもここに書きます。
「どう処理するかを考える」のがServiceの仕事です。
4.3. Repositoryについて
Repositoryは、レストランでいう倉庫担当です。
シェフから依頼を受けると、食材庫(データベース)から必要な食材(データ)を取り出して渡します。
Spring Bootでは、データベースからデータを取得したり、保存したりする役割です。
// RepositoryであることをSpring Bootに伝える
@Repository
public class UserRepository {
public List<User> findAll() {
// データベースからユーザー一覧を取得する
// (実際はSQLを実行したり、Spring Data JPAなどを利用します)
return ...
}
}
Repositoryの仕事は、「データベースにあるデータの取得・変更」です。
-
データを取得する(SELECT)
-
データを保存する(INSERT)
-
データを更新する(UPDATE)
-
データを削除する(DELETE)
それ以外の処理は基本的には行いません。
割引計算や在庫チェックなどの業務ロジックはRepositoryには書かず、Serviceで行います。
5. まとめ
Spring Bootでは、クラスを「Controller」「Service」「Repository」の3つの役割に分けて実装することが一般的です。
これを3層アーキテクチャと呼びます。
役割を分けずに開発を続けると、一つのクラスが多くの責任を持つ「神クラス」が生まれることがあります。
すると、「コード量が多くて目的の処理が見つからない」「一つの変更が意図しないエラーを引き起こす」「複数人での開発で不便」などの問題が発生します。
この問題を解決するのが、クラスの分割です。
レストランで例えると、Controllerはウェイター、Serviceはシェフ、Repositoryは倉庫担当です。
ウェイターが注文を受け付け、シェフが調理をし、倉庫担当が必要な食材を管理・調達します。
同じように、ControllerはHTTPリクエストの受け取りとServiceへの処理の依頼、Serviceは業務ロジック(アプリケーションの処理)、Repositoryはデータベースとのやり取りをそれぞれ担当しています。
ブラウザ
「HTTPリクエスト」
↓
Controller
「リクエスト受付・Serviceへ処理を依頼」
↓
Service
「業務ロジックを実行・Repositoryへデータ取得を依頼」
↓
Repository
「データベースからデータを取得・更新」
↓
Database
「データを返す」
↓
Repository
「取得したデータをServiceへ返す」
↓
Service
「必要な処理を行い、Controllerへ返す」
↓
Controller
「HTTPレスポンスを返す」
↓
ブラウザ
「画面に表示」
一見すると、クラスが増えて複雑になったように感じるかもしれません。
しかし、レストランが役割分担によって効率よく営業できるように、Spring Bootもクラスごとに役割を分けることで、保守しやすく、変更に強いアプリケーションを実現しています。
これが、Spring Bootで3層アーキテクチャが採用されている理由です。
6. おわりに
今回は、
「Spring BootではなぜクラスをController、Service、Repositoryに分けるの?」
という疑問から始まり、
「『@Controller』や『@Service』は必ず書かなければいけないの?」
「別々のクラスはどうやって連携しているの?」
「Repositoryはどうやってデータベースとやり取りしているの?」
というように、一歩踏み込んだ疑問を持てるようになったのではないでしょうか。
この記事を通して、「何となくお作法として書いていたコード」が、「なぜその設計になっているのか」を理解するきっかけになっていたら嬉しく思います。
次に重要になるのが、「分かれたクラス同士をどうつなぐか」という仕組みです。
Spring Bootでは、この仕組みを「DI(依存性注入)」と呼びます。
次回は、このDIについて整理したいと思います。
もし「自分はこう理解した」「こっちの方がわかりやすい」という意見があれば、ぜひコメントで教えてください!

