一覧一つにクエリ301本
目標
Springが代わりにやってくれることを見ていきます。検証・例外の変換・トランザクション境界・遅延ロードはすべて目に見えない場所で起こるため、問題も目に見えません。
特に最後の1つ、つまり一覧を1つ表示するのにクエリが何本出るかを実際に数えます。
環境
Java 21・Maven・Spring Boot 3.4.1・H2(メモリ)。ネットワークはありません。
mkdir -p /root/work/spring
cp -r /opt/lab/samples/spring-starter/* /root/work/spring/
cd /root/work/spring
mvn -o -B test # -o 를 빠뜨리면 원격을 보러 가서 실패합니다
依存関係は、スケルトンのpom.xmlにあるものだけが使えます(web、validation、data-jpa、actuator、h2、test)。新しい依存関係はダウンロードできません。
契約
採点ツールはクラスを参照せず、HTTPとクエリ数だけで判定します。クラスをどう分けても、以下を満たせば問題ありません。
| パス | 契約 |
|---|---|
GET /healthz |
{"status":"ok"} |
POST /items |
{name, price}・正常は200/201・違反は400 |
GET /items |
保存された一覧(JSON配列) |
GET /items/{id} |
あれば200・なければ404とメッセージ |
POST /items/bulk |
配列・1件でも失敗したらすべてロールバック |
GET /shops |
店ごとの品物数・クエリは2本以下 |
パッケージはlabhubにしてください。
ステップ
- スケルトン・
/healthz @Valid→ 400@RestControllerAdvice→ 404- コンストラクター注入
@Transactionalのロールバック- N+1を数えて減らす →
06-queries.txt @WebMvcTestのスライス- まとめ →
08-notes.md
参考
採点ツールは、プロジェクトをコピーに移して検査します。srcに採点用のファイルが残ることはありません。
プロジェクトを立ち上げる
/root/work/springにSpring Bootプロジェクトを作成し、GET /healthzが{"status":"ok"}を返すようにしてください。パッケージはlabhubです。
スケルトンが用意されています: mkdir -p /root/work/spring && cp -r /opt/lab/samples/spring-starter/* /root/work/spring/。ネットワークがないため、常に-o(オフライン)を付けます: mvn -o -B test。スケルトンのpomにない依存関係はダウンロードできません。
検証をフレームワークに任せる
POST /itemsを作成し、リクエスト本文を@Validで検証してください。名前が空、または価格が負の値なら400、正常なら200/201を返す必要があります。
record ItemRequest(@NotBlank String name, @Min(1) int price)を使います。@Validを付けないと、制約アノテーションは何もしません。付けてあるのに検証が働かないケースの大半がこれです。if (name == null)と書いているなら、フレームワークの仕事を奪っています。
例外を1か所でステータスコードに変換する
GET /items/{id}で存在しないidには404とメッセージを返すようにしてください。処理は@RestControllerAdviceの1か所にまとめます。
コントローラーごとにtry-catchを散らすと、どの例外がどのコードで返るのか、誰にもわからなくなります。return ResponseEntity.ok(Map.of("error", ...))で200を返してはいけません。ステータスコードは契約であり、プロキシやモニタリングがそれを見て動作します。
コンストラクター注入に変える
@Service層を置き、フィールドに@Autowiredを付けないでください。コンストラクター注入だけを使います(private final)。
コンストラクターが1つなら、@Autowired自体が不要です。得られるもの: finalにできる、テストでそのまま渡して差し替えられる、依存関係がいくつあるか目に見える、そして循環参照が起動時に失敗することです。運用中に失敗するよりましです。
一括処理が失敗したらすべて元に戻す
POST /items/bulkで配列を受け取って保存してください。1件でも不正ならすべてロールバックされ、すべて正常ならすべて保存される必要があります。
@Transactionalをサービスメソッドに付けます。注意点は3つあります。チェック例外はデフォルトではロールバックされず(rollbackForが必要)、同じクラスの中からthis.method()で呼び出すとプロキシを経由しないため無視され、privateメソッドには付きません。
クエリを数えて減らす
店と品物を1:Nの関係にして、GET /shopsが各店の品物数も返すようにしてください。クエリ数を計測し、修正前と修正後を06-queries.txtに書いて、最終的に2本以下にしてください。
application.propertiesにspring.jpa.properties.hibernate.generate_statistics=trueを入れ、テストでStatistics.getPrepareStatementCount()を使って数えます。まずfindAll()で実装して、店の数だけクエリが余分に出ることを確認してから、@Query("select distinct s from Shop s join fetch s.items")で1本に減らしてください。数え方を知ることは、直し方を知ることより重要です。
Web層だけを起動するテスト
@WebMvcTestでコントローラーだけを検証するテストを1つ書いてください。サービスは@MockitoBeanで代わりに差し込みます。
@WebMvcTestはサービスBeanを起動しないため、モックを差し込まないとコンテキストが起動しません。MockMvcで呼び出します。コンテキスト全体を毎回起動するとテストが遅くなり、遅くなると誰も実行しなくなります。
見えなかった3つのこと
08-notes.mdに3行以上書いてください。なぜコンストラクター注入なのか、N+1がなぜ起こり何で直したのか、@Transactionalが何を元に戻すのか(そして戻さない場合)を書きます。
本文に생성자、N+1、롤백が含まれている必要があります(韓国語の2つの語は、それぞれ「コンストラクター」と「ロールバック」を意味します)。ステップ6の2つの数字も併せて書いておくと、後で同じコードを見たときに判断が速くなります。