コース一覧
Spring Boot入門
3層アーキテクチャ

Spring Boot入門

Java で Web API サーバーを作るための Spring Boot の入門コースです。DI コンテナ、REST コントローラ、リクエスト検証、レイヤ分割、Spring Data JPA、例外ハンドリング、テストまでを扱います。毎レッスン、Spring が裏側でやっている判断そのものを pure Java の関数として自分で書き、フレームワークが何を肩代わりしてくれているのかを手触りで理解できる構成にしました。約 12 時間 (1 日 30 分 × 24 日) で 48 レッスンを修了できます。

1
Spring Bootとは何か
0. Spring Bootで何が作れるのか10分
1. サーブレットからフレームワークへ15分
2. ルーティング表という発想15分
3. プロジェクトの構成10分
4. 起動のしくみと自動設定15分
5. パスのマッチング15分
2
DIコンテナ
0. newで書くと何が困るのか15分
1. コンストラクタインジェクション15分
2. インターフェースへの依存15分
3. @Componentと@Bean15分
4. 自作のDIコンテナ20分
5. Beanのスコープとライフサイクル15分
3
RESTコントローラ
0. RESTという設計方針15分
1. @RestControllerの役割15分
2. @GetMappingとパス変数15分
3. クエリパラメータの受け取り15分
4. @PostMappingとリクエストボディ15分
5. ステータスコードの選び方15分
6. ResponseEntityで組み立てる15分
4
リクエストとレスポンスの変換
0. JSONとオブジェクトの往復15分
1. DTOとエンティティを分ける15分
2. recordでDTOを書く15分
3. 入力検証の基本15分
4. 検証エラーをまとめて返す15分
5. アノテーションによる検証15分
5
レイヤ構成とビジネスロジック
0. 3層アーキテクチャ10分
1. @Serviceに置くもの15分
2. 状態遷移をルールにする15分
3. 料金計算のような業務ロジック15分
4. @Transactionalとは何か15分
5. トランザクションの巻き戻し15分
6
データアクセス
0. @Entityとテーブルの対応15分
1. リポジトリという抽象15分
2. メソッド名からクエリを作る20分
3. 検索条件の組み立て15分
4. ページングと並び替え15分
5. N+1問題15分
7
例外処理と横断的関心事
0. 例外をHTTPに変換する15分
1. @ExceptionHandlerと@ControllerAdvice15分
2. エラーレスポンスの形を決める15分
3. フィルタとインターセプタ15分
4. ログとリクエストの追跡15分
5. 設定値の外部化15分
8
テストと仕上げ
0. テストの種類と使い分け15分
1. Serviceの単体テスト15分
2. モックで依存を差し替える15分
3. MockMvcでエンドポイントを試す15分
4. 本番へ出すための次の一歩10分

Spring Boot入門

01Spring Bootで何が作れるのか
02サーブレットからフレームワークへ
03ルーティング表という発想
04プロジェクトの構成
05起動のしくみと自動設定
06パスのマッチング
07newで書くと何が困るのか
08コンストラクタインジェクション
09インターフェースへの依存
10@Componentと@Bean
11自作のDIコンテナ
12Beanのスコープとライフサイクル
13RESTという設計方針
14@RestControllerの役割
15@GetMappingとパス変数
16クエリパラメータの受け取り
17@PostMappingとリクエストボディ
18ステータスコードの選び方
19ResponseEntityで組み立てる
20JSONとオブジェクトの往復
21DTOとエンティティを分ける
22recordでDTOを書く
23入力検証の基本
24検証エラーをまとめて返す
25アノテーションによる検証
263層アーキテクチャ
27@Serviceに置くもの
28状態遷移をルールにする
29料金計算のような業務ロジック
30@Transactionalとは何か
31トランザクションの巻き戻し
32@Entityとテーブルの対応
33リポジトリという抽象
34メソッド名からクエリを作る
35検索条件の組み立て
36ページングと並び替え
37N+1問題
38例外をHTTPに変換する
39@ExceptionHandlerと@ControllerAdvice
40エラーレスポンスの形を決める
41フィルタとインターセプタ
42ログとリクエストの追跡
43設定値の外部化
44テストの種類と使い分け
45Serviceの単体テスト
46モックで依存を差し替える
47MockMvcでエンドポイントを試す
48本番へ出すための次の一歩

Spring Boot入門

3層アーキテクチャ

このレッスンでは、Controller と Service と Repository がそれぞれ何を引き受ける層なのかを説明でき、依存の向きと層をまたぐときの型の扱いを判断できるようになります。

層を分けないと何が起きるでしょうか

最初はコントローラ1つで全部書けます。動くものが早くできるので、悪い選択には見えません。

Java

@RestController public class OrderController { private final JdbcTemplate jdbc; @PostMapping("/orders/{id}/ship") public ResponseEntity<String> ship(@PathVariable Long id) { Map<String, Object> row = jdbc.queryForMap("select * from orders where id = ?", id); String status = (String) row.get("status"); if (!"approved".equals(status)) { return ResponseEntity.badRequest().body("cannot ship"); } int total = (int) row.get("total"); int fee = total >= 5000 ? 0 : 500; jdbc.update("update orders set status = 'shipped', fee = ? where id = ?", fee, id); return ResponseEntity.ok("shipped"); } }

この1つのメソッドの中に、性質のまったく違う3種類の仕事が同居しています。HTTP の話(パス変数の受け取り、ステータスコードの決定)、業務の話(approved でなければ出荷できない、5000 円以上は送料無料)、保存の話(SQL の発行)です。

困るのは、変更の理由が3つとも別々にやってくる点です。送料の無料ラインが 3000 円に下がる、テーブルを分割する、レスポンスの形式を JSON に変える。これらはまったく無関係な出来事なのに、同じメソッドを触ることになります。しかも「出荷できるのは approved のときだけ」という業務の決まりが、HTTP を組み立てないと呼び出せない場所に埋まっているため、単体テストを書こうとするとサーバーを起動する話になります。

3つの層と、それぞれが引き受けるもの

層を分けるとは、変更の理由ごとにクラスを分けることです。Spring Boot の定番は次の3層です。

層主なアノテーション引き受けること引き受けないこと
Controller@RestControllerHTTP の受け口。パスとメソッドの対応、パラメータの取り出し、検証の起動、ステータスコードの決定、DTO への詰め替え業務ルールの判断、SQL
Service@Service業務ルール。可否の判定、計算、複数のリポジトリ操作をまとめた一連の手続き、トランザクション境界HTTP の型、SQL の文字列
Repository@Repository / JpaRepository永続化。保存、検索、削除業務ルールの判断

覚え方としては、それぞれの層が「知らなくてよいこと」を挙げられるかどうかが目安になります。Service は自分が HTTP から呼ばれたのかバッチから呼ばれたのかを知りません。Repository は、なぜその行を保存するのかを知りません。知らなくても仕事ができる状態が、分離できている状態です。

先ほどのコードを分けると、次のような形になります。

Java

@RestController public class OrderController { private final OrderService orderService; OrderController(OrderService orderService) { this.orderService = orderService; } @PostMapping("/orders/{id}/ship") public ResponseEntity<OrderResponse> ship(@PathVariable Long id) { Order shipped = orderService.ship(id); return ResponseEntity.ok(OrderResponse.from(shipped)); } } @Service public class OrderService { private final OrderRepository orderRepository; OrderService(OrderRepository orderRepository) { this.orderRepository = orderRepository; } @Transactional public Order ship(Long id) { Order order = orderRepository.findById(id) .orElseThrow(() -> new OrderNotFoundException(id)); order.ship(); return orderRepository.save(order); } }

コントローラのメソッドが3行になりました。ここには判断が1つもありません。受け取って、渡して、返すだけです。これが健全な Controller の姿です。

依存の向きは上から下への一方向です

層の分け方と同じくらい大事なのが、どちらがどちらを知っているかという向きです。正しい向きは Controller から Service へ、Service から Repository へ、の一方向だけです。

プレーンテキスト

Controller ---> Service ---> Repository ---> DB

逆向きの矢印は引きません。Service が Controller を呼ぶことも、Repository が Service を呼ぶこともありません。この一方通行を守ると、次の3つが得られます。

  1. 下の層だけを取り出してテストできる — Service のテストに Controller は要りません
  2. 下の層を差し替えられる — Repository の実装を JPA から別のものへ変えても、Service の中身は変わりません
  3. 循環しない — 相互に呼び合うクラスができないので、どこから読み始めればよいかが決まります

2番目を確実にするには、Service が Repository のインターフェースに依存し、実装クラスを知らない形にします。JpaRepository を継承した自作インターフェースを Service が受け取る書き方が、まさにこれです。実装は Spring が実行時に用意します。

矢印が逆を向きたくなる場面は実際にあります。たとえば Repository の中で「保存前に金額を検算したい」と思うときです。これは業務ルールなので、Repository ではなく Service か、金額を持つドメインオブジェクト自身の仕事です。矢印を逆にしたくなったら、置き場所を間違えたと考えてください。

層をまたぐときは型を乗り換えます

各層は扱う型も違います。同じ「注文」でも、層によって呼び名と形が変わります。

層使う型形を決める都合
外部との境界OrderRequest / OrderResponse などの DTOクライアントが必要とするもの
Serviceドメインオブジェクト(Order)業務ルールの表現しやすさ
Repositoryエンティティ(@Entity の付いたクラス)テーブル設計

小規模なアプリではドメインオブジェクトとエンティティを同じクラスで兼ねることが多く、それは現実的な妥協として広く行われています。しかし DTO だけは分けます。理由は、レスポンスの形をテーブル設計に引きずられたくないからです。

詰め替えを書く場所にも決まりがあります。DTO からドメインへ、ドメインから DTO への変換は Controller 側で行います。Service の引数と戻り値に OrderRequest のような DTO を使ってしまうと、Service が HTTP の都合を知ってしまい、バッチや別の入口から再利用できなくなります。Service に渡すのはドメインオブジェクトか、id や金額のような素の値です。

なぜ Controller に業務ロジックを書かないのでしょうか

理由を4つに整理します。

  1. テストの重さが違う — 「5000 円以上は送料無料」の確認に、HTTP リクエストの組み立ては要りません。Service の static でないメソッドを直接呼べるなら、テストは1行で書けて一瞬で終わります
  2. 入口が増えたときに複製される — 同じ処理を管理画面からも、定期バッチからも呼びたくなったとき、ロジックが Controller にあるとコピーするしかありません。コピーした瞬間から、片方だけ直される未来が確定します
  3. トランザクション境界を引く場所がない — @Transactional は Service のメソッドに付けるのが基本です。複数の保存をまとめて成功か失敗かにしたいとき、その単位は業務の手続きの単位であり、HTTP リクエストの単位ではありません
  4. 読む人が探す場所が定まらない — 「送料はどこで決まるのか」と聞かれたとき、Service を見れば分かる、と即答できる状態には価値があります

Controller が薄いかどうかは、次の問いで確かめられます。そのメソッドから if を全部取り除いても意味が変わらないでしょうか。取り除けないなら、その if は Service へ移すべき判断です。

この節で書いていくもの

この後のレッスンでは、Service に置くべき判断を pure Java の static メソッドとして書いていきます。Spring では @Service を付けたクラスのメソッドとして書き、DI で受け取って呼ぶものですが、中身の判断そのものはフレームワークとは無関係な素の Java です。逆に言えば、その部分だけを取り出して書けるということ自体が、層が分かれている証拠でもあります。

このレッスンに出てくる用語

意味があいまいなまま進んだ語は、ここから読み直せます。

  • 判断YES/NO 分岐を表す菱形
  • メソッドクラスに属する関数
  • HTTPWeb の通信プロトコル、HTTPS は TLS で暗号化したもの
  • 変数データに名前をつけて参照する仕組み
  • ステータスコード200/201/404 など結果を示す3桁の数値
  • SQLデータベースを操作するための共通言語
  • テーブルDB の表 (Excel のシートみたいなもの)
  • リクエストWeb 通信の基本単位、ブラウザの問い合わせとサーバーの返答
生田 陸人
監修生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア監修者プロフィールを見る →
編集 ゆめさく編集部·公開 2026/08/09

関連レッスン

  • @Serviceに置くもの

    業務ルールを Service に集約する形を実装します。

  • 状態遷移をルールにする

    許される状態変化だけを通す判定を実装します。

  • 料金計算のような業務ロジック

    条件が絡む計算を、読める形で実装します。

  • @Transactionalとは何か

    トランザクション境界とロールバックの条件を確認します。

分からないところは Tap (AI先生) に質問できます

24 時間いつでも、あなたのレベルに合わせて日本語で答えます。