Spring Boot プロジェクト構成 — 10 人規模で崩れないパッケージ設計
Controller / Service / Repository の責務境界、ドメイン層の切り方、テストの並べ方まで。Spring Boot API 開発で「後から直せなくなるコード」を防ぐためのパッケージ構成の指針。

なぜ 「パッケージ構成」 の話をするのか
Spring Boot で API を書き始めると、最初の 1 ヶ月は爆速で進みます。3 ヶ月後、 人が増えて 「Service にビジネスロジック以外もまぎれ始める」、 「どこに何を書けばいいかメンバーで揃わない」、「テストが書きにくい箇所が出てくる」 — だいたいこの頃にパッケージ構成の歪みが現れます。
パッケージ構成はドキュメントの代わり になります。構造を見ただけで「どこに何を書けばよいか」が自明になっている構成は、 新メンバーのオンボーディングにも、レビューの速度にも直接効きます。
本稿では、Glossvation で複数の業務システムに適用して崩れにくかった構成を紹介します。 正解ではなく、たたき台 として読んでください。
大枠: 4 層 + 共通モジュール
com.example.api/
├── App.java # @SpringBootApplication
├── controller/ # 入口 (HTTP)
│ ├── v1/
│ └── v2/
├── domain/ # ビジネスの中核
│ ├── model/ # 不変の値オブジェクト・DTO
│ ├── entity/ # 永続化 (DB) に対応するクラス
│ ├── repository/ # 永続化の抽象 (interface + impl)
│ └── service/ # ユースケース
├── exception/ # 独自例外 + @ControllerAdvice
├── security/
│ ├── filter/ # 認証フィルタ
│ └── role/ # ロール定義
├── common/
│ ├── config/ # @Configuration
│ ├── property/ # @ConfigurationProperties
│ ├── filter/ # アプリ全体に効く Filter
│ └── util/ # ステートレスな計算
└── task/ # @Scheduled ジョブ
テストも同じ構造で:
src/test/java/com/example/api/
├── AppTests.java
├── controller/
├── domain/
│ ├── service/
│ └── repository/
└── common/
各層の責務
controller/ — HTTP の変換層
@RestControllerを置く- ビジネスロジックを書かない (バリデーションの橋渡しと Service 呼び出しに専念)- API バージョンは
v1/v2/で物理的に分離 (@RequestMappingだけでなくパッケージで) - レスポンス DTO は controller パッケージに置く (domain と混ぜない)
@RestController
@RequestMapping("/v1/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<OrderResponse> create(@Validated @RequestBody OrderCreateRequest req) {
var order = orderService.create(req.toCommand());
return ResponseEntity.ok(OrderResponse.from(order));
}
}
domain/service/ — ユースケース層
- 1 ユースケース = 1 メソッド (
createOrder,cancelOrder…) @Transactionalは ここに付ける (controller や repository ではなく)- 外部通信 (API, DB) は repository / infra に委ねる
- テスト対象として最も重要 → 単体テスト率を高く保つ
domain/repository/ — 永続化の抽象
- interface は domain 配下に置く (ドメインの言葉で定義)
- JPA / MyBatis / R2DBC などの実装はここに
- 外部 API クライアントもここに置く (DB だけが永続化ではない)
domain/model vs domain/entity
model/= ドメイン内で引き回す値 (不変、record推奨、DB 非依存)entity/= JPA / 永続化フレームワーク固有のクラス (@Entity等)
このふたつを混ぜると、永続化の都合 (@ManyToOne, lazy loading) がドメインに染み出して疎結合が崩れます。
変換関数 (UserEntity → User) を repository 境界で挟むのが鍵。
common/ — アプリ横断
config/— Bean 定義 (@Configuration)property/—@ConfigurationPropertiesでまとめた設定クラスutil/— static メソッドのみ、状態を持たない
common を 「何でも置き場」 にしない。分類できないものは、別の層に属する責務を見逃しているサインです。
exception/ — エラー変換
- 独自例外 (
OrderNotFoundException等) を定義 @RestControllerAdviceで HTTP ステータスに変換する箇所を集約- ドメイン例外 → HTTP の変換ロジックは 1 ヶ所に閉じ込める
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<ErrorResponse> handle(OrderNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of("ORDER_NOT_FOUND", e.getMessage()));
}
}
DI の流れ (依存の向き)
Controller → Service → Repository (interface)
↓ ↑
Domain ← 実装 (JPA/MyBatis)
依存は 「内向き」 に流す (= Controller 側がドメインに依存し、ドメインは Controller を知らない)。 これを守ると、Web 層やフレームワーク差し替えが容易になります (いわゆる Hexagonal / Clean Architecture の考え方)。
落とし穴
1. Service がファット化する
1 メソッドが 300 行超えたら黄信号。 ユースケースの粒度を見直し、ドメインサービスか、専用 Command/Query クラスに切り出します。
2. DTO を domain に置く
DTO は Controller 入出力のための データ転送用。
ドメインモデルと混ぜると、API 契約の変更がドメインに波及します。
Controller パッケージ配下に *Request, *Response として置くのが基本。
3. 共通処理を Controller/Service に書く
ログ、認可、監査などは AOP か Filter / Interceptor に外す。 Controller/Service に書くと重複して保守不能になります。
4. application.yml にシークレットを書く
本番の認証情報は 環境変数 + ${...} で参照、
または AWS Secrets Manager / Vault / Spring Cloud Config 経由で注入。
application-prod.yml にベタ書きしない。
5. バージョンアップ時の @Deprecated ゾンビ
Spring Boot 2 → 3 でパッケージ名が javax.* → jakarta.* に大変化しました。
古い @Deprecated を放置せずに 移行の都度削除 します。
近代化アップデート (2017 → 2024+)
| 当時 | 現代の推奨 |
|---|---|
| Spring Boot 1.x / 2.x | Spring Boot 3.x (Java 17+, Jakarta EE 9+) |
Lombok @Data | record (不変 DTO) + lombok は補助に |
| 手書き Builder | @Builder (Lombok) or record + toBuilder() |
WebSecurityConfigurerAdapter | 廃止。SecurityFilterChain Bean で定義 |
JUnit 4 + @RunWith(SpringRunner.class) | JUnit 5 + @SpringBootTest (単体テストは @MockBean ではなく @MockitoBean or Mockito 直接) |
| Executor の手動設定 | Spring Boot 3.1+ の仮想スレッド spring.threads.virtual.enabled=true を検討 |
まとめ: パッケージ構成は 「こう決めた」 と文書化するだけでチームの速度が変わります。本稿の構造は 1 つの叩き台ですが、責務の境界を明文化し、新規コードを書くときの迷いを消す こと自体が重要です。
関連記事
- Web・モバイル·
Spring AOP 実装パターン — トランザクション・ログ・権限を本流から剥がす
横断的関心事 (クロスカッティングコンサーン) を綺麗に分離する Spring AOP の使い所と Aspect/JoinPoint/Advice/Pointcut の全体像、プロダクションで効くユースケース 4 選。
- Web・モバイル·
Spring Bean ライフサイクル — スコープ・初期化順・落とし穴
BeanDefinition のロードから @PostConstruct まで、Spring Bean がどの順序で生成されるかを 7 ステップで整理。Singleton/Prototype/Request/Session スコープの違いと実務的な使い分けも。
- Web・モバイル·
Java パフォーマンスチューニングの実務ノート — JVM / コレクション / GC
プリミティブ型・不変オブジェクト・コレクション選択・ストリーム vs for ループ・GC チューニングまで、現場で効くチューニングの型。計測してからチューニングする原則を軸に。