[Spring Boot] 3つのパッケージ戦略の特徴とメリット・デメリット

はじめに

Spring Bootを用いたアプリケーション開発において、パッケージ構成は単なるフォルダ整理ではなく、アーキテクチャ上の設計思想を具体化する要素です。どのようにコードを構造化するかは、プロジェクトの保守性、拡張性、可読性、チーム内理解のしやすさに直結します。

多くの開発チームでは、Spring Bootの標準的なチュートリアルに倣い、コントローラ、サービス、リポジトリといったレイヤーごとの構成を採用します。一方で、ドメイン駆動設計(DDD)の普及により、ビジネスロジックの独立性と変更容易性を重視したドメイン分割型構成も一般的になっています。さらに近年では、これらを組み合わせたハイブリッド型構成が注目され、モジュラモノリスやマイクロサービス移行を見据えた実装が増えています。

これら三つの戦略は、それぞれに長所と短所があり、プロジェクト規模、チーム体制、ドメインの複雑性によって最適な選択は異なります。レイヤー分割型は理解しやすい一方で大規模化に弱く、ドメイン分割型は拡張性に優れるものの初期設計コストが高く、ハイブリッド型は柔軟性が高い反面、設計負荷が大きいという特徴があります。

本記事では、以下の三つの戦略について解説します。

  • レイヤー分割型
  • ドメイン分割型
  • ハイブリッド型(Layer × Domain)

それぞれの概要、メリット・デメリット、向いているケースを整理したうえで、共通の評価軸による比較と選定指針を提示します。目的は、パッケージ構成を「慣習」ではなく「設計上の意図」として説明できる状態を確立することです。

初期段階の構造設計は、後のスケーラビリティと生産性を大きく左右します。本稿が、Spring Boot開発における合理的かつ再現性のある構造選択の一助となること幸いです。

レイヤー分割型パッケージ戦略(Layered Architecture)

レイヤー分割型パッケージ戦略は、Spring Bootにおいて最も広く採用されている構成手法の一つです。システムを技術的な責務ごとに明確に分離することを目的としており、アプリケーションの基本構造を理解しやすくする点に特徴があります。

概要

レイヤー分割型は、アプリケーションを役割ごとに分離するアーキテクチャであり、主に以下のレイヤーで構成されます。

  • コントローラ層(Web層)
  • サービス層(ビジネスロジック層)
  • リポジトリ層(データアクセス層)
  • モデル層(ドメインオブジェクトやエンティティ)

この構成では、上位レイヤーが下位レイヤーに依存する単方向の依存関係が形成され、典型的には「Controller → Service → Repository」という流れで処理が進みます。MVC(Model-View-Controller)やN層アーキテクチャの概念を踏襲しており、Spring Frameworkが提供する依存性注入(DI)やトランザクション管理と非常に相性が良い点が特徴です。

パッケージ構成の一例を示すと、以下のようになります。

com.example.project
 ├── controller
 ├── service
 ├── repository
 ├── model
 └── config

この構成により、同一レイヤーに属するコンポーネントが集約され、機能的な関心ごとに整理された明確な責務分離を実現します。

メリット・デメリット

レイヤー分割型には明確な利点と課題があります。以下に主なポイントを整理します。

メリット

  • 構造が単純で理解しやすい
  • 責務分離が明確でチーム内統一が容易
  • AOPやトランザクション管理などの横断的関心を適用しやすい
  • テスト時にモック化が容易で単体テストの実施がしやすい

デメリット

  • サービス層にビジネスロジックが集中しやすい
  • 特定ドメインに関連するコードが複数レイヤーに分散しやすい
  • システム規模の拡大に伴い、変更影響範囲が広がる
  • レイヤー間依存が強く、モジュール分割やマイクロサービス化が困難

レイヤー分割型は、構造を理解しやすく、特に新規開発者や小規模チームにとって効果的です。レイヤー単位でAOPや例外処理を統一できる点も管理上の利点です。

一方で、業務ドメイン単位の関心を横断的に扱うことが難しく、特にビジネスロジックが複雑化する場合には、構造が急速に肥大化します。その結果、特定機能の修正やリファクタリングに多大な影響を及ぼすことがあります。また、構造が技術指向であるため、ドメインの意味的なまとまりが見えにくく、設計上の抽象度が上がりにくい点も課題です。

向いているケース・プロジェクト特性

レイヤー分割型は、次のような条件を持つプロジェクトに適しています。

  • 小〜中規模の業務システム
  • チーム構成が少人数またはフルスタック開発中心
  • 技術レイヤー単位での責務分離を重視するプロジェクト
  • 開発スピードを優先し、設計複雑度を抑えたいケース

この構成は、Spring Bootの標準構造に最も近く、学習コストが低いという利点があります。そのため、システムのドメインが単純であり、開発者間で統一的な理解が求められるプロジェクトでは特に有効です。ただし、長期運用や複雑な業務ロジックを含む場合は、後述するドメイン分割型またはハイブリッド型への移行を視野に入れることが望ましいです。

ドメイン分割型パッケージ戦略(Domain-based Architecture)

ドメイン分割型パッケージ戦略は、ビジネスドメインを中心にパッケージ構造を設計する手法です。技術的な関心ごとではなく、業務上の意味的なまとまりを基準にアプリケーションを構成することで、変更容易性と独立性の高い設計を実現します。

概要

ドメイン分割型は、アプリケーションを「業務領域(ドメイン)」単位で構成し、それぞれのドメイン内にコントローラ、サービス、リポジトリなどの技術要素を内包します。これにより、ドメインごとに自己完結的な構造が形成され、変更や拡張をドメイン単位で閉じることが可能になります。典型的な構成例は以下のとおりです。

com.example.project
 ├── order
 │   ├── controller
 │   ├── service
 │   ├── repository
 │   └── model
 ├── customer
 │   ├── controller
 │   ├── service
 │   ├── repository
 │   └── model
 └── shared
     ├── config
     └── util

この構成では、各ドメインが独立した小さなアプリケーションのように機能します。ドメイン間の依存関係は最小限に抑えられ、共通的な要素は shared や common パッケージとして抽出されます。ドメイン駆動設計(Domain-Driven Design:DDD)の思想と親和性が高く、集約やエンティティなどのモデリング概念を実装上で明示的に表現できる点が特徴です。

メリット・デメリット

ドメイン分割型の利点と課題を整理すると、次のようになります。

メリット

  • ドメイン単位での変更やテストが容易
  • ビジネスロジックがドメイン内部に閉じ、責務が明確
  • モジュラリティが高く、将来的なマイクロサービス化に適する
  • コード構造が業務構造を反映し、関係者間での共通理解が促進される

デメリット

  • 初期設計コストが高く、ドメインモデリングの理解が必要
  • 小規模プロジェクトでは構造が冗長になりやすい
  • 共通処理の抽出・配置に関する設計判断が難しい
  • ドメイン間依存を適切に制御しないと再び結合度が高まる

ドメイン分割型の最大の利点は、変更容易性と保守性の高さです。各ドメインが独立した構造を持つため、他の領域への影響を最小限に抑えて機能を修正・追加できます。

また、ドメイン単位でテストを完結できるため、単体・統合テストの効率化にもつながります。さらに、業務の概念構造とコード構造が一致することで、非技術者を含む関係者間でのコミュニケーションが容易になります。

一方で、この手法を効果的に運用するには、チーム全体がドメインモデリングの基本原則を理解している必要があります。初期設計段階でドメイン境界を適切に定義できない場合、パッケージ間依存が複雑化し、かえって保守性を損なうリスクがあります。また、シンプルな業務アプリケーションに適用すると構造が過剰になり、開発コストが増加する傾向があります。

向いているケース・プロジェクト特性

ドメイン分割型は、次のような条件を持つプロジェクトに適しています。

  • 中〜大規模の業務システム
  • 業務ドメインが複数存在し、それぞれの変更頻度が高いシステム
  • チームがドメイン駆動設計(DDD)の基本概念を理解している環境
  • 将来的にモジュール分割やマイクロサービス化を見据えたプロジェクト

この戦略は、ビジネス構造をコード上に直接反映したい場合に最も効果的です。特に、長期運用を前提としたシステムや、複数チームが異なるドメインを並行して開発する環境では、高い独立性と保守性を確保できます。ただし、初期導入時にはモデリングコストと設計負荷が伴うため、適用範囲とスコープを明確に定めたうえで段階的に導入することが望ましいです。

ハイブリッド型パッケージ戦略(Layer × Domain)

ハイブリッド型パッケージ戦略は、レイヤー分割型とドメイン分割型の双方の特性を組み合わせた構成手法です。技術的責務の明確さとドメイン単位の独立性を両立させることで、柔軟性と保守性の高いアーキテクチャを実現します。

概要

ハイブリッド型は、外部からの入出力やアプリケーション全体に共通する処理を「レイヤー構造」で整理し、ビジネスロジックや業務領域を「ドメイン単位」で構築する構成です。Clean ArchitectureやHexagonal Architectureなどの考え方と親和性が高く、依存方向を「外部 → 内部」に限定することにより、アプリケーションの中心にドメインモデルを据えることができます。典型的な構成例は以下のとおりです。

com.example.project
 ├── api
 │   ├── order
 │   └── customer
 ├── domain
 │   ├── order
 │   ├── customer
 │   └── shared
 ├── infrastructure
 │   ├── database
 │   ├── messaging
 │   └── config
 └── application
     └── scheduler

この構成では、API層(またはプレゼンテーション層)が外部との接点を担当し、ドメイン層がビジネスロジックを保持します。インフラストラクチャ層は技術的な依存要素(データベース、メッセージング、外部API接続など)を扱い、依存関係は常にドメイン層に向かうよう設計されます。この構造により、ドメイン中心の拡張を可能にしつつ、外部I/O処理を明確に分離することができます。

メリット・デメリット

ハイブリッド型の利点と課題を整理すると、以下のようになります。

メリット

  • ドメインごとの独立性と技術層の責務分離を両立
  • Clean ArchitectureやHexagonal Architectureとの整合性が高い
  • モジュラリティが高く、将来的なマイクロサービス化に適する
  • テスト容易性が高く、レイヤー単位・ドメイン単位の両方で検証可能

デメリット

  • 初期設計コストが高く、構造理解に一定の知識が必要
  • パッケージ間の依存関係設計を誤ると循環依存を生じやすい
  • 小規模チームでは運用負荷が高く、過剰設計になるリスクがある
  • 開発メンバー間で設計原則を共有できない場合、構造が崩壊しやすい

ハイブリッド型の最大の特徴は、アーキテクチャ全体をドメイン中心に設計できる点です。ドメイン層がビジネスルールを担い、外部要素はあくまで入出力の手段として扱われるため、技術的変更(フレームワークやデータソースの切り替え)に対して高い柔軟性を持ちます。また、各ドメインが独立しているため、機能追加や修正が他領域に波及しにくく、長期的な保守性を維持しやすい構造となります。さらに、アプリケーション層・インフラ層・ドメイン層といった多層的な観点でテストを設計できるため、品質保証の観点でも優れています。

一方で、ハイブリッド型は高度な設計判断を要するため、チーム全体でアーキテクチャ原則を統一できない場合には運用が困難になります。特に、ドメイン層の境界や依存関係の方向性を誤ると、構造が複雑化しやすく、結果的にレイヤー型やドメイン型の利点を損なう可能性があります。設計段階で依存方向やパッケージ間契約を明文化し、アーキテクト主導でガイドラインを整備することが成功の鍵となります。

向いているケース・プロジェクト特性

ハイブリッド型は、次のような条件を持つプロジェクトに適しています。

  • 中〜大規模の業務システム
  • 長期的な保守・拡張を前提とするプロジェクト
  • 複数チームや役割分担が明確な開発体制
  • ドメイン駆動設計(DDD)やクリーンアーキテクチャの理解を有する組織
  • 将来的にモジュラモノリスやマイクロサービスへ移行を計画しているシステム

この戦略は、技術的関心ごとと業務ドメインの双方を体系的に管理したい場合に有効です。特に、システムが進化的に拡張される前提を持つ場合、ハイブリッド型は長期的なアーキテクチャ安定性を確保する上で最も有効な手法といえます。ただし、構造の柔軟性と抽象度が高いため、初期導入時には明確な設計ガイドラインを策定し、全開発者が依存関係の原則を共有することが不可欠です。

3つの戦略の比較と選定指針

この章では、これまで解説した3つのパッケージ戦略(レイヤー分割型/ドメイン分割型/ハイブリッド型)を共通の評価軸で比較・整理します。

まず総合比較表を提示し、その後に各評価軸の意味と考慮すべき設計上の観点を解説します。

パッケージ戦略の評価

各パッケージ戦略の特徴をもとに評価すると以下のようになります。

評価軸レイヤー分割型ドメイン分割型ハイブリッド型
保守性中規模まで良好。レイヤー単位で変更容易高い。ドメイン単位で独立保守可能高い。依存方向を制御しつつ拡張可能
拡張性横断的な機能追加に強いドメイン追加・変更に強い両者のバランスを取れる
可読性初学者に理解しやすい構造業務ドメイン理解者に明快構造が複層的でやや難解
モジュラリティ低い(レイヤー間依存が強い)高い(疎結合な構造にしやすい)非常に高い(モジュール分離前提設計)
テスト容易性統合テスト中心ドメイン単位のユニットテスト容易両者を組み合わせ可能
チーム適合度小〜中規模チームに適す大規模・専門分化チームに適す成熟した組織・アーキテクト主導型に適す
マイクロサービス移行適性低い高い高い
初期導入コスト低い中〜高高い
運用・教育コスト低い高い(共通理解が必要)

評価軸の解説

  • 保守性 ー 変更時の影響範囲と修正コスト
    構造的にどの単位で変更を閉じ込められるかを示します。 ドメイン分割型・ハイブリッド型はドメイン境界で修正を完結できる点で優位です。
  • 拡張性 ー 新機能・新ドメイン追加のしやすさ
    レイヤー型は横断的(技術的)な追加に向き、ドメイン型は業務単位の拡張に強いです。 ハイブリッド型は両方向に拡張可能です。
  • 可読性 ー 新規参入者が構造を理解しやすいかどうか
    技術レイヤー単位の整理は直感的ですが、ビジネス構造は見えにくいです。 ドメイン構成はその逆となります。
  • モジュラリティ(独立性) ー 各モジュールの疎結合性と再利用性
    ドメイン/ハイブリッド型は、将来のモジュール分割・サービス分離を前提に設計しやすいです。
  • テスト容易性 ー テストの単位と独立性
    ドメイン分割・ハイブリッド型ではユニットテスト・集約単位テストが自然に構築可能です。
  • チーム適合度 ー チーム規模・専門性との相性
    レイヤー型は少人数開発に、ドメイン・ハイブリッド型はドメインごとの担当分化がある組織に向きます。
  • マイクロサービス移行適性 ー 将来的にドメイン単位で分割・独立展開しやすいか
    レイヤー型はモノリシック前提、ドメイン/ハイブリッドは移行が容易です。
  • 初期導入コスト ー 設計と理解に要する初期負担
    レイヤー型はテンプレート化しやすいですが、ドメイン/ハイブリッド型はモデリングと合意形成が必要です。
  • 運用・教育コスト ー チーム全体で構造を理解・維持する難易度
    ハイブリッド型は高度な設計文化を前提とするため、教育・ドキュメント整備が不可欠です。

まとめ:選定の考え方

最適な戦略は「チームの成熟度 × ドメインの複雑性 × システムの寿命」で決まります。

  • 短期開発・単機能中心:レイヤー分割型
  • 中〜長期・業務ドメイン中心:ドメイン分割型
  • 複数ドメイン・長寿命・進化設計志向:ハイブリッド型

パッケージ戦略は固定的なルールではなく、設計成熟度の指標で、チームの成長に合わせて、段階的に構造を進化させることが望ましいです。

おわりに

本記事では、Spring Bootにおける三つの代表的なパッケージ戦略であるレイヤー分割型、ドメイン分割型、ハイブリッド型について、それぞれの特徴とメリット・デメリット、そして適用に向くプロジェクト特性を整理しました。いずれの戦略も有効な設計手法であり、優劣ではなく目的と文脈によって最適解が異なります。

レイヤー分割型は、シンプルな構造と習熟のしやすさにより、小規模かつ短期開発プロジェクトで高い生産性を発揮します。ドメイン分割型は、業務構造をコードに反映し、長期的な保守性と変更容易性を重視するシステムに適しています。ハイブリッド型はその両者を統合し、技術的責務とビジネスドメインのバランスをとりながら柔軟な拡張を可能にします。

重要なのは、パッケージ構成を単なるフォルダ整理や慣例的な設計手法として扱わないことです。構造はアーキテクチャの意図を示す表現であり、チームがどのような価値観と設計思想のもとに開発を進めるかを具現化するものです。どの戦略を採用する場合でも、構造の意味と依存関係の方向性を明確にし、チーム全体で共有することが不可欠です。

システムの成長とともに最適な構造は変化します。初期段階ではレイヤー型で始め、ドメイン分割型やハイブリッド型へ段階的に進化させることも有効なアプローチです。パッケージ構成を設計思想の延長として捉え、アーキテクチャの一部として継続的に見直していくことが、堅牢で持続可能なシステムを実現する鍵となります。

【Java】List.of()にnullを渡すとどうなる?安全なリストの構築方法

Java 9から追加されたList.of()は、簡潔に不変リスト(immutable list)を作成できる便利なAPIです。しかし、使い方を間違えるとNullPointerExceptionを引き起こす落とし穴があります

本記事では、List.of()nullを渡した場合の挙動と、安全にリストを構築する方法について解説します。

List.of()にnullを渡すとどうなるか

以下のようにnullを含む値をList.of()に渡すと、実行時に例外が発生します。

String a = "hello";
String b = null;

List<String> list = List.of(a, b); // ← ここで例外が発生

実行結果:

Exception in thread "main" java.lang.NullPointerException
    at java.base/java.util.ImmutableCollections.listFromTrustedArray(...)

List.of()は「null非許容」です。これは公式ドキュメントにも明記されています。

List (Java SE 9 & JDK 9 )から引用

安全な方法1:Stream.of() + filter(Object::nonNull)

nullを除外してリストを構築したい場合は、Stream APIを使って次のように書けます:

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

String a = "hello";
String b = null;

List<String> list = Stream.of(a, b)
    .filter(Objects::nonNull)
    .collect(Collectors.toList()); // → ["hello"]

この方法ではnullを安全に取り除いたリストを作成できます。List.of()と異なり変更可能なリストになりますが、多くの場合は問題ありません。

安全な方法2:手動でnullチェックして追加

もっと素朴な方法として、以下のようにif文で追加するのも実用的です:

import java.util.ArrayList;
import java.util.List;

String a = "hello";
String b = null;

List<String> list = new ArrayList<>();
if (a != null) list.add(a);
if (b != null) list.add(b); // → ["hello"]

処理の過程でnullを記録・無視・ログ出力したい場合など、柔軟性があります。

nullを含める必要がある場合は?

どうしてもnullを含む必要がある場合は、List.of()を使うのではなく、通常のArrayListArrays.asList()を使いましょう。

List<String> list = Arrays.asList("hello", null); // OK

ただし、Arrays.asList()は固定サイズリストを返すため、要素の追加・削除はできない点に注意してください。

✅ まとめ

方法null許容リスト型備考
List.of(a, b)不変リストnullを含むと例外
Stream.of(a, b).filter(…).collect()可変リストnullを除外して簡潔に書ける
手動で if != null で追加可変リスト柔軟な制御が可能
Arrays.asList(…)固定サイズ要素の追加削除不可

Javaでnullを扱う場面はまだ多くあります。List.of()は便利ですが、nullは許容されない」という仕様を正しく理解した上で使い分けることが大切です

Spring FrameworkにおけるDIのベストプラクティス: コンストラクタインジェクションの優位性

Spring Frameworkでの依存性注入には、

  • フィールドインジェクション
  • コンストラクタインジェクション
  • セッターインジェクション

の3つの方法があります。これらの方法は、オブジェクトが他のオブジェクトに依存している場合に、その依存オブジェクトを自動的に挿入するために使用されます。この中で主に使われるのはフィールドインジェクションとコンストラクタインジェクションです。

Springのコミュニティでは、フィールドインジェクションよりもコンストラクタインジェクションの方が推奨されています。なぜコンストラクタインジェクションが推奨されているのかをフィールドインジェクションのデメリットとコンストラクタインジェクションのメリットの観点から見ていきましょう。

フィールドインジェクション

フィールドインジェクションは、クラスのフィールドに直接依存性を注入する方法です。これは@Autowiredアノテーションをフィールドに適用することで行われます。例えば、

@Component
public class MyService {
    @Autowired
    private DependencyClass dependency;
}

のように書きます。

コンストラクタインジェクション

コンストラクタインジェクションは、オブジェクトのコンストラクタを通じて依存性を注入する方法です。コンストラクタに@Autowiredアノテーションを付けることで、Springが自動的に依存オブジェクトを挿入します。例えば、

@Component
public class MyService {
    private final DependencyClass dependency;

    @Autowired
    public MyService(DependencyClass dependency) {
        this.dependency = dependency;
    }
}

のように書きます。

コンストラクタインジェクションが推奨される理由

Spring Framework でコンストラクタインジェクションが推奨される理由のうち、特に理由としてふさわしいと思ったものには以下が2つがあります。

依存コンポーネントの不変性を宣言できる

前述のコード例を見てもらうとわかりますが、フィールドインジェクションではオブジェクト生成時点でフィールドの値が初期化されないため、final宣言をつけることができません。一方、コンストラクタインジェクションではオブジェクト生成時点でフィールドの値が初期化されるため、final宣言をつけることができます。

これにより依存コンポーネントがイミュータブル(不変)であることを明示することができるようになります。

実際にイミュータブルであることで誤って再初期化してしまうことを防ぐことはほとんどないと思いますが、実際に変更してしまうかどうかは別として、変更するつもりがないという設計意図をコードに表現できることが重要だと考えています。

循環依存を回避できる

コンストラクタインジェクションを使用するとコンストラクタの呼び出し時点でインジェクションされてイミュータブルになります。このとき循環依存があると、アプリケーションの起動時にエラーが発生して循環依存を検知することができます。

一方、フィールドインジェクションの場合は実際に対象のフィールドが呼び出されるまで検知できないため、循環依存の発見が遅れたり、発見自体が困難になる場合があります。

単一責任の原則に従っていないことに気づきやすくなるのか

単一責任の原則に従っていないことに気づきやすくなるというメリットを挙げているのを見かけますが、個人的にはそれほど効果がないのではないかと思っています。

まったくないとは言いませんが、Lombokを使用していると、ほぼフィールドインジェクションのような使い心地でコンストラクタインジェクションを使えてしまうので、レビューで検出する方が確実だと思います。例えば、

@Component
@RequiredArgsConstructor
public class MyService {
    private final DependencyClass dependency;
}

のように@RequiredArgsConstructorアノテーションを使用することで、コンストラクタを省略することが可能です。これ自体はボイラープレートコードを書かなくて済むというメリットがありますが、コンストラクタ自体を書かないため、依存コンポーネントが多すぎることに気づきにくいと思います。

コンストラクタインジェクションに頼るのではなく、きちんとレビュー前のセルフチェックやレビューで依存関係の複雑さや単一責任の原則に従っているのかなどをチェックすることが重要だと考えています。

単体テストコードを書きやすくなるのか

元々はコンストラクタインジェクションの方が書きやすかったと思うのですが、現在ではほとんど変わらないと思いました。

フィールドインジェクションを使ったコンポーネントの例

フィールドインジェクションを使ったコンポーネントのテストを書く例として、次のようなサービスクラスに対するテストケースを考えてみましょう。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SampleService {

    @Autowired
    private DummyComponent dummyComponent;

    public String doSomething() {
        return dummyComponent.doSomething();
    }
}

このサービスクラスに対するテストは以下のようになります。

package com.example.demo;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class SampleServiceTest {

    @InjectMocks
    private SampleService sampleService;

    @Mock
    private DummyComponent dummyComponent;

    @Test
    public void testDoSomething() {
        when(dummyComponent.doSomething()).thenReturn("test");

        assertEquals("test", sampleService.doSomething());
    }
}

コンストラクションインジェクションを使ったコンポーネントの例

コンストラクタインジェクションを使ったコンポーネントの例も見てみましょう。同じサービスクラスをコンストラクタインジェクションで書き換えた場合、以下のようになります。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SampleService {

    private final DummyComponent dummyComponent;

    @Autowired
    public SampleService(DummyComponent dummyComponent) {
        this.dummyComponent = dummyComponent;
    }

    public String doSomething() {
        return dummyComponent.doSomething();
    }
}

このサービスクラスに対するテストは以下のようになります。

package com.example.demo;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class SampleServiceTest {

    @InjectMocks
    private SampleService sampleService;

    @Mock
    private DummyComponent dummyComponent;

    @Test
    public void testDoSomething() {
        when(dummyComponent.doSomething()).thenReturn("test");

        assertEquals("test", sampleService.doSomething());
    }
}

見ての通り同じテストケースクラスでテストできます。

現状ではテストのしやすさという点ではその差はほぼないと考えて差し支えないと思います。

まとめ

依存コンポーネントに対して不変性を明示でき、循環依存を回避できるようになる点を考慮すると、コンストラクタインジェクションを積極的に使用していくことは重要だと思います。

一方で、単一責任の原則に従っているかどうか、依存関係の多いファットなコンポーネントになっていないかどうかについては、ペアプログラミング時に議論したり、セルフチェックやレビューによってチェックすることが重要だと思います。

[Spring Boot]IntelliJ IDEAでLombokを使っていてエラーになる場合の対処方法

以下の記事に解決方法が載っていました。

https://stackoverflow.com/questions/72583645/compile-error-with-lombok-in-intellij-only-when-running-build

遭遇した事象

Spring Initializrからダウンロードした時点でLombokは依存関係に追加されています。

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.0.4'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.t0k0sh1'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.projectlombok:lombok:1.18.22'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

IntelliJ IDEAでLombokを使用可能にするためには、設定のビルド、実行、デプロイ>コンパイラー>アノテーションプロセッサーで、「アノテーション処理を有効にする」にチェックをつけ、「プロジェクトクラスパスからプロセッサーを取得する」にチェックがついている状態にします。

ここまでがよく知られているLombokの導入方法になります。

lombok.Dataアノテーションを定義したクラスを作成し、

package com.t0k0sh1.tutorial.entity;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String name;
    private String email;
    private String password;
}

自動生成されているであろうgetterを使ってみます。

package com.t0k0sh1.tutorial.controller;

import com.t0k0sh1.tutorial.entity.User;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
public class UserController {
    private final List<User> users = new ArrayList<>();

    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        return users.stream().filter(a -> a.getId().equals(id)).findFirst().orElse(null);
    }

}

すると、getterが見つからずにコンパイルエラーとなります。

/Users/t0k0sh1/Workspace/tutorial/src/main/java/com/t0k0sh1/tutorial/controller/UserController.java:15: エラー: シンボルを見つけられません
        return users.stream().filter(a -> a.getId().equals(id)).findFirst().orElse(null);
                                           ^
  シンボル:   メソッド getId()
  場所: タイプUserの変数 a

対処方法

build.gradleを以下のように書き換えることで問題を解消することができます。

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.0.4'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.t0k0sh1'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.projectlombok:lombok:1.18.22'
	implementation 'org.modelmapper:modelmapper:3.1.1'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	// 以下の1行を追加する
	annotationProcessor 'org.projectlombok:lombok:1.18.22'
}

tasks.named('test') {
	useJUnitPlatform()
}

追加でannotationProcessor 'org.projectlombok:lombok:1.18.22'を追記します。(バージョン部分は元の記述に合わせてください)

Gradleプロジェクトの再ロード(右端のGradleタブを開いて更新ボタンをクリックする)し、プロジェクトのビルドを行なってください。

すると、先ほどまでエラーとなっていましたが、今度はビルドに成功します。

[Spring Boot]プロジェクトを作成する(Spring Initializr)

Spring Bootのプロジェクトの作成方法はいくつかありますが、本記事ではSpring Initializrを使って作成します。

Projectを選択する

執筆時点では「Gradle – Groovy」「Gradle – Kotolin」「Maven」から選択できます。使いたいものを選択してください。

ここでは「Gradle – Groovy」を選択しています。

Languageを選択する

執筆時点では「Java」「Kotolin」「Groovy」から選択できます。使いたいものを選択してください。

ここでは「Java」を選択しています。

Spring Bootのバージョンを選択する

Spring Bootのバージョンを選択してください。

ここでは「3.0.4」を選択しています。

Project Metadataを入力する

プロジェクトのメタデータを入力します。

内容はプロジェクトに合わせて設定してください。

Dependenciesを選択する

Dependenciesの選択は任意ですが、Webアプリケーションのプロジェクトを作成するので、Spring Webだけ追加しておきます。すでに使用するものが決まっている場合はそれも追加しておいてください。

プロジェクトをダウンロードする

GENERATEボタンをクリックしてプロジェクトをダウンロードしてください。

プロジェクトを展開・配置してIDEで開く

ではダウンロードしたプロジェクトを展開・配置して、IDEで開いてみましょう。ここではIntelliJ IDEA Ultimateを使用しています。

まずはダウンロードしたZIPファイルを展開し任意のフォルダに配置します。

$ unzip ~/Downloads/tutorial.zip -d ~/Workspace/

次にIDEでプロジェクトを開きます。

動作確認のためにアプリケーションを実行してみましょう。

アプリケーションが起動したらhttp://localhost:8080/にアクセスします。

Spring BootではMainメソッド持ったクラス以外何もないため、表示するページがなく、エラー画面が表示されますが、この画面が表示しているのであれば問題ありません。

[Java]ラムダ式を使ってコレクションを操作する(Java Lambda)

Java 8から導入されたラムダ式を使ってコレクションを操作する方法について解説します。ここでは、実用的なものに的を絞って解説しています。

前提(説明時に使用しているDTO)

説明で使用しているDTOを提示しておきます。ここではUserDtoクラスを使用しています。

import java.io.Serializable;

public class UserDto implements Serializable {
    private String userId;
    private String userName;
    private int age;

    // Setter, Getterは省略

    public String toString() {
        return "userId=" + userId + ",userName=" + userName + ",age=" + age;
    }

}

ListをMapに変換する

ListをMapに変換する方法について解説します。

以下の例では、UserDtoクラスを要素に持つListをMapに変換しています。MapのキーはUserDtoのuserId、値はUserDtoにしています。

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        // UserDtoを要素として持つListを用意する
        List<UserDto> list = new ArrayList<>();
        UserDto user1 = new UserDto();
        user1.setUserId("001");
        user1.setUserName("John Smith");
        user1.setAge(20);
        list.add(user1);
        UserDto user2 = new UserDto();
        user2.setUserId("002");
        user2.setUserName("Maria Cambell");
        user2.setAge(28);
        list.add(user2);
        
        // ListをMapに変換する(キーはUserDTOのUserIdを使用する)
        Map<String, UserDto> map = list.stream().collect(Collectors.toMap(s -> s.getUserId(), s -> s));

        // Listの出力結果を確認する
        System.out.println(list);
        // [userId=001,userName=John Smith,age=20,userId=002, userName=Maria Cambell,age=28]
        
        // 変換後のMapの出力結果を確認する
        System.out.println(map);
        // {001=userId=001,userName=John Smith,age=20, 002=userId=002,userName=Maria Cambell,age=28}
    }
}

Listから一部の項目を抽出する

Listから一部の項目を抽出する方法について解説します。

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        // UserDtoを要素として持つListを用意する
        List<UserDto> list = new ArrayList<>();
        UserDto user1 = new UserDto();
        user1.setUserId("001");
        user1.setUserName("John Smith");
        user1.setAge(20);
        list.add(user1);
        UserDto user2 = new UserDto();
        user2.setUserId("002");
        user2.setUserName("Maria Cambell");
        user2.setAge(28);
        list.add(user2);

        // Listから項目の一部(ここではUserDtoのuserIdを抽出)し、リストにする
        List<String> userIds = list.stream().map(s -> s.getUserId()).collect(Collectors.toList());
  
        
        // 抽出前のListの内容を確認する
        System.out.println(list);
        // [userId=001,userName=John Smith,age=20, userId=002,userName=Maria Cambell,age=28]
        // 抽出した項目を確認する
        System.out.println(userIds);
        // [001, 002]
    }
}
モバイルバージョンを終了