Javaのカプセル化とMVC設計:なぜ「public」だらけになるのか?
Javaを学ぶ過程で「カプセル化(隠蔽)」という概念に出会いますが、実際にMVCなどのレイヤードアーキテクチャで開発を始めると、理想と現実のギャップに直面し、
「あれ? カプセル化って隠すことだよね? なのにMVCで作るとほとんどのクラスをpublicにしないと動かない…これって設計として正解なの?」
という疑問から、パッケージの範囲とアクセス修飾子という観点、「MVC設計のジレンマ」について記事としてまとめました。
PS.なぜ今頃カプセル化について疑問に思ったかというと、ミノ駆動さんの改訂新版 良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方を読んでおり、あれ???となったことがきっかけです。良書なので、ジュニアからミドルになる層は読んだほうがいいと思います。
カプセル化とアクセス修飾子の基本ルール
Javaにおけるカプセル化は、アクセス修飾子を使って「どこから見えるか」を制限することで実現されます。まずは改めて整理しましょう。
ここで注目すべきは、「何も書かない(パッケージプライベート)」という選択肢です。これは「同じパッケージ(フォルダ)内のクラス同士なら自由にアクセスできるが、外からは隠す」という、Javaが提供する強力な隠蔽機能です。
そもそもパッケージの「範囲」と物理的な境界って?
Javaの「パッケージ」は、ドメインやプロジェクト全体ではなく、「同じソースフォルダ(ディレクトリ)構造を共有している範囲」を指します。
- 同じパッケージ: 物理的に同じフォルダにあるクラス同士。修飾子なしでアクセス可能。
-
別のパッケージ: 物理的に違うフォルダにあるクラス同士。
publicがないと一切アクセス不可能。
つまり、MVC構成のように com.app.controller と com.app.service にパッケージを分けた時点で、それらの間には「物理的な壁」が作られることになります。
では、なぜMVCだと「public」だらけになるのか?
MVCでは役割ごとにパッケージを分けるため、必然的に別のパッケージ間でのクラス利用が発生します。
- Controller は Service を呼び出す。
- Service は Repository を呼び出す。
これらは全て「別のパッケージ」に属しているため、ControllerがServiceを呼ぶためには、Service側で public を宣言しなければなりません。結果、本来は「Service内部で閉じたい処理」まで、プロジェクト全体から呼べる public メソッドになってしまうのです。
「publicだらけ問題」とどう向き合うか?
このジレンマに対して、実務では以下の3つのステップで対処します。
ステップ1:「publicは契約である」と割り切る
すべてを隠すのがカプセル化ではありません。public は「このクラスの機能として外部(他のレイヤー)に提供するAPI」という契約だと考えます。Controllerから呼ばれるメソッドは public で正解です。しかし、Service内部でしか使わない補助的なメソッドは、きちんと private にする。この意識を持つだけで、カプセル化は十分機能します。
ステップ2:インターフェースによる隠蔽
Serviceの実装詳細を徹底的に隠したい場合は、インターフェースを導入します。
-
UserService(インターフェース)をpublicにする。 -
UserServiceImpl(実装クラス)を 修飾子なし(パッケージプライベート) にする。
これによって、Controllerはインターフェースしか見えないため、実装クラス内の詳細なコードに触れることは物理的にできなくなります。
ステップ3:モジュールシステム(Java 9+)
より厳格に管理したい場合は、module-info.java を使います。公開したくないパッケージを「エクスポート」しない設定にすることで、public クラスであっても外部からアクセスさせないという、強固な防御が可能になります。
まとめ:カプセル化の目的を見失わない
MVC設計において「publicが増えること」は、必ずしも設計の失敗ではありません。むしろ、「どこを公開して、どこを隠すか」という設計意図(API設計)を明確にするチャンスです。
- 小規模: インターフェースとアクセス修飾子の使い分けで十分。
- 大規模: モジュールシステムまで活用し、依存関係を厳格化する。
「カプセル化」という言葉に縛られすぎず、「どの範囲まで見せれば、プログラムの結合度が適切に保てるか?」という視点を持つことが、MVCを使いこなす第一歩です。