アクセス修飾子の選択ミスが招くJava設計トラブル集

Javaの public, private, protected, 無指定(package-private)といったアクセス修飾子は、
一見単純な構文選択に見えて、設計の責務や構造の健全性に直結する要素です。

しかし実際には「テストのために public にした」「とりあえず protected」「無指定だったのを忘れてた」など、
設計上の意図を持たずに選ばれているケースが多く、その結果、後から大きな構造破綻や保守負債を生むことがあります。

本記事では、現場で頻出するアクセス制御の失敗パターンをもとに、レビューすべき設計観点を明確にしていきます。

アクセス修飾子の設計的役割

Javaのアクセス修飾子は、単に「外からアクセスできるか」を決めるものではありません。
クラスの責務がどこに向けて公開されているか、または隠蔽すべきかを、構文上で明示する手段です。

アクセス修飾子別:設計意図と選択基準

修飾子 可視範囲 主な設計用途
public すべてのクラスからアクセス可能 外部との契約・API公開
protected サブクラス + 同一パッケージ 継承による拡張責務
(無指定) 同一パッケージのみアクセス可能 パッケージ内の構造共有、密結合回避
private 同一クラス内のみアクセス可能 実装詳細の隠蔽、補助ロジックの封じ込め
判断の基本

アクセス修飾子は可視性の設定ではなく、「責務が公開されるべきかどうか」の設計判断です。

以降、この修飾子判断を誤ることで発生する実際のトラブルを見ていきましょう。

ケース1:private にしすぎてテスト不能になる

症状

  • 単体テストでprivateメソッドを直接検証したいがアクセスできない
  • 仕方なく public に変更 → 本番でAPIとして誤認されてしまう
public class TokenService {
    private String hash(String input) {
        ...
    }
}

問題の本質

  • アクセス制御の問題ではなく、構造の分離ができていない
  • 責務をユーティリティや別クラスに追い出せば public にする必要がなくなる
テストのためのpublic化は“設計敗北”

テストしやすさは構造で担保すべき。修飾子の緩和は設計の代償行為になっていないか、レビューで確認が必要です。

ケース2:protectedを“内部用”と誤解して仕様を晒す

症状

  • 継承される前提がないのに protected にしてしまい、外部で勝手に継承されて仕様が漏れる
  • 想定外の拡張でセキュリティや仕様変更に影響が及ぶ
public class TokenGenerator {
    protected String buildSecret() { ... } // 外部から継承・呼び出し可能に
}

問題の本質

  • 「継承前提か否か」の判断がなく、設計契約のつもりが意図せず公開に
  • Javaでは protected = 継承 + パッケージアクセス。事実上のpublic化になる可能性がある

ケース3:package-privateがリファクタで破綻

症状

  • パッケージ内で閉じる設計だったが、構成整理で別パッケージに分離 → アクセスエラー続出
  • 急遽 public に変更 → 層境界やドメインモデルの破壊
class DiscountPolicy {
    int calculate(int price) { ... } // パッケージ内限定のつもりが...
}

問題の本質

  • package-privateは強力な構造制御だが、パッケージ構成変更に脆弱
  • パッケージ設計の成熟度と密接に関わるため、長期設計では protectedpublic との併用判断が重要

ケース4:インターフェースがpublicで実装がpackage-private

症状

  • APIとして public interface を定義したが、実装クラスを class XxxImpl のまま無指定にしてしまい、DIでは動くが明示的に使えない
public interface Formatter {
    String format(String input);
}

class HtmlFormatter implements Formatter {
    ...
}

→ Springでは注入できるが、new HtmlFormatter() が書けない → テストや一部ユースケースで混乱

インターフェースと実装の公開範囲はセットで設計

APIとして公開するなら、実装が必要な場合にどう提供するか(Factory, DIなど)まで構造に含めて設計すべきです。

ケース5:実装をpublicにしすぎて責務が流出する

症状

  • ライブラリやユーティリティクラスが何でもかんでも public → 呼び出し側が内部実装に依存
  • 実装の差し替えや削除が困難に
public class DateUtil {
    public static String formatDate(Date d) { ... }
    public static long parseCustomEpoch(String raw) { ... } // 本来内部用
}

→ parseCustomEpochを使うコードが乱立 → 削除不可の仕様化

publicにした瞬間、それは“API”になる

レビューでは「これは本当に公開されるべき責務か?」を、契約としての公開という観点で評価すべきです。

レビュー観点:アクセス制御が責務と一致しているか?

チェックリスト

レビュー観点

アクセス修飾子は“可視性”ではなく“設計契約”の表明である。
責務がどこまで開かれるべきか、その設計判断が構文レベルで表現されているかを確認する。

まとめ:アクセス修飾子は「都合」ではなく「契約」で選ぶ

  • テストの都合で public にする
  • 継承しないのに protected
  • 無指定のまま構造が壊れる

こうした設計ミスは、「コードが動くかどうか」ではなく、構造として成立しているかどうかで判断されるべきです。

アクセス修飾子は設計の“意志”をコードで明示する手段です。
レビューアーはその構文の裏にある設計意図を読み取り、「設計と可視性のズレ」がないかを常に評価すべきです。

次回は、「継承より委譲が設計上優れるケース」について構造・責務の観点から解説します。