1. いつ・なぜプロジェクトをモジュールに分割するのか
すべてを単一モジュールに詰め込むべきでない理由
小さな課題や "Hello, World!" だけを書くのであれば、モジュールシステムは過剰に見えるかもしれません。しかし、プロジェクトが大きくなるにつれて—数十・数百のクラス、数多くのパッケージ、外部ライブラリ—混沌は避けられません。棚のない図書館のようなものです。本が少ないうちは何とかなっても、その先は目当ての本を見つけるのが難しくなります。モジュールはあなたの棚です。秩序をもたらし、実装という「裏側」を隠して、外側には「ショーケース」(API)だけを見せます。
モジュール分割のメリット
- 責務の分離: 各モジュールは自分の領域(例: DB、ビジネスロジック、UI)を担当する。
- コードの再利用: モジュールは別プロジェクトからも利用できる。
- テスタビリティの向上: モジュールを独立にテストできる。
- セキュリティとカプセル化: 外部に見えるのは API のみで、実装は隠蔽される。
- 保守容易性の向上: 「魔法の」依存が減り、依存関係のマップが明確になる。
- ビルド・デプロイの高速化: 変更されたモジュールだけを再ビルドする。
いつ分割すべきか
- プロジェクトが開発者1人では扱いきれない規模になった(または IDE が重くなってきた)。
- 明確な部分が見えてきた: core, ui, utils, api, impl。
- コードを他プロジェクトで再利用する計画がある。
- 特定の部分だけが必要とする外部依存がある。
- 実装の詳細(アルゴリズム、内部クラス)を隠したい。
2. モジュール化の典型パターン
以下は学習用・実務用のどちらにも適した代表的な分割パターンです。
「オニオンアーキテクチャ」(Onion Architecture)
外側の層は内側に依存しますが、その逆はありません。
[ app (UI) ]
↓
[ core (ロジック) ]
↓
[ utils (ユーティリティ) ]
- app — 外側のモジュール。グラフィカル UI、Web アプリ、エントリポイント(main)。
- core — ビジネスロジック、モデル、サービス。
- utils — 補助的なクラス群。
ルール: 内側の層は依存してはならない(外側に)。このため、core はさまざまなインターフェース(コンソール、Web、デスクトップ)で再利用できる。
API と実装を分離するモジュール
ライブラリでは、インターフェースとその実装を別に分けると便利です:
[ mylib.api ] ← インターフェースのみをエクスポート
[ mylib.impl ] ← 実装を含む。エクスポートしない
テスト用モジュール
テストは本番アーティファクトに混入させないため、別モジュールに分けることがよくあります.
[ app ]
[ core ]
[ core.tests ]
学習用プロジェクトの例
myeditor/
├─ app/ ← エントリポイント、アプリの起動
├─ core/ ← ビジネスロジック(ファイルやテキストの処理)
└─ utils/ ← ユーティリティ(ログ出力、パース)
3. モジュール間の依存関係
Java のモジュールでは、依存関係は module-info.java に requires を使って明示します。これにより可読性が上がり、コンパイラ/JVM が exports を通じて API の可視性を制御できます。
依存関係の例
core/module-info.java
module myeditor.core {
exports myeditor.core.api; // 外部に公開するのは api パッケージのみ
requires myeditor.utils; // ユーティリティを利用
}
app/module-info.java
module myeditor.app {
requires myeditor.core; // core を利用
requires myeditor.utils; // ユーティリティを直接使ってもよい
}
ルールとベストプラクティス
- 循環依存を避ける。 A が requires で B に依存し、B も requires で A に依存しているのは設計の欠陥。通常は共通の common/api に切り出して解決する。
- 依存は最小限に。 本当に必要でないモジュールは追加しない。
- 利用するパッケージをエクスポートする。 クラスは exports で宣言したパッケージに置く。そうでないとコンパイルエラーになる。
- ユーティリティは最大限独立に。 utils はビジネスロジックに依存してはならない。
4. 実践: 学習用プロジェクトを 3 モジュールに分割する例
ディレクトリ構成
myeditor/
├─ app/
│ ├─ src/
│ │ └─ myeditor/app/Main.java
│ └─ module-info.java
├─ core/
│ ├─ src/
│ │ ├─ myeditor/core/api/TextService.java
│ │ └─ myeditor/core/impl/TextServiceImpl.java
│ └─ module-info.java
└─ utils/
├─ src/
│ └─ myeditor/utils/Logger.java
└─ module-info.java
module-info.java の例
core/module-info.java
module myeditor.core {
exports myeditor.core.api;
requires myeditor.utils;
}
app/module-info.java
module myeditor.app {
requires myeditor.core;
requires myeditor.utils;
}
utils/module-info.java
module myeditor.utils {
exports myeditor.utils;
}
コード例(TextService)
myeditor/core/api/TextService.java
package myeditor.core.api;
public interface TextService {
String toUpperCase(String text);
}
myeditor/core/impl/TextServiceImpl.java
package myeditor.core.impl;
import myeditor.core.api.TextService;
public class TextServiceImpl implements TextService {
@Override
public String toUpperCase(String text) {
return text.toUpperCase();
}
}
myeditor/app/Main.java
package myeditor.app;
import myeditor.core.api.TextService;
import myeditor.core.impl.TextServiceImpl;
public class Main {
public static void main(String[] args) {
TextService service = new TextServiceImpl();
System.out.println(service.toUpperCase("hello, modules!"));
}
}
IntelliJ IDEA での見え方
- 各ディレクトリはプロジェクト構成上の独立した Module になる。
- 各モジュールは src の直下に独自の module-info.java を持つ。
- app の main を実行すると、IDE が module-path を自動で解決する。
- エクスポートされていないパッケージのクラスを使おうとするとコンパイルエラーになる。
5. ビルドへの影響: Maven/Gradle とモジュール
Maven
マルチモジュールプロジェクトは「親」プロジェクト(parent)と複数の「子」モジュールで構成される。
myeditor/
├─ pom.xml ← parent
├─ app/
│ └─ pom.xml
├─ core/
│ └─ pom.xml
└─ utils/
└─ pom.xml
parent pom.xml の例
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>myeditor</groupId>
<artifactId>myeditor-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>app</module>
<module>core</module>
<module>utils</module>
</modules>
</project>
特徴:
- Maven はコンパイル時に module-info.java を考慮する。
- 実行時は --classpath ではなく --module-path を使用する。
- exports や requires を忘れるとコンパイルエラーになる。
Gradle
マルチモジュールは settings.gradle と各モジュールの個別の build.gradle で設定する。
settings.gradle
rootProject.name = 'myeditor'
include 'app', 'core', 'utils'
モジュール用の build.gradle
plugins {
id 'java'
}
java {
modularity.inferModulePath = true
}
IntelliJ IDEA
- IDEA は Java モジュール作成時に module-info.java を生成できる。
- Maven/Gradle を使う場合、モジュール構成は自動的に取り込まれる。
- app の main を実行すると、IDE が module-path を設定する。
- インポート/エクスポートのダイアログで、パッケージやモジュールの可視性を確認できる。
モジュール分割でよくあるミス
エラー No.1: モジュール間の循環依存。 2 つのモジュールが互いに requires していると、コンパイラはエラーを出します。これはアーキテクチャが崩れているサインです。解決策 — 共通の api モジュールを切り出すか、境界を見直す。
エラー No.2: エクスポートされていないパッケージのクラスを使用。 クラスが public でも、パッケージが module-info.java の exports に記載されていなければ、別モジュールからは見えません。結果としてコンパイルエラーになります。
エラー No.3: 利用しているモジュールに対する requires を追加し忘れる。 別モジュールからの import があっても、対応する記述が module-info.java になければコンパイルできません。依存関係は常に明示しましょう。
エラー No.4: モジュール名の重複。 モジュール名はビルドの範囲で一意である必要があります(特に Maven/Gradle)。重複はビルドを壊します。
エラー No.5: ディレクトリ構成の誤り。 ファイル module-info.java は該当モジュールの src 直下に置く必要があります。そうでないとコンパイラはモジュールを見つけません。
エラー No.6: 実行時の module-path 設定ミス。 手動で実行する場合は --module-path を使い、--classpath の代わりに指定してください。そうしないと「module not found」になります。
GO TO FULL VERSION