「共通化」には、性質のまったく異なる2種類があります。ビジネスロジックの共通化と、ログ・認証・認可・例外処理といった横断的処理の共通化です。前者はなるべく避けるべきで、後者はフレームワーク機能を活用して積極的に行うべきです。この2つを混同することが、コードベースを負債に変える一因になります。
第1章:ビジネスロジックの共通化はなるべくしない
思考の放棄としての common パッケージ
「良かれと思って作った共通処理」が負債になるケースがあります。重複コードなら修正コストが増えるだけですが、誤った共通化による密結合はシステムを修正不能な状態に追い込みます。
common や util といったパッケージが肥大化するのは、「この処理が何の業務ドメインに属するか」「誰のための機能か」という設計の思考を放棄しているからです。こうした共通処理は以下のような末路を辿ります。
- 影響範囲の肥大化: 呼び出し元が把握できなくなり、わずかな修正がシステム全体に波及する。
- スパゲッティ化: 特定の呼び出し元向けの分岐が増え、コードが複雑化する。
共通化の設計の第一歩は、「common という名前をやめる」ことです。役割や業務ドメインを体現する名前を考え抜く必要があります。
DRYの誤解を解く
DRY(Don't Repeat Yourself)は「同じコードを繰り返すな」と解釈されがちです。しかし、DRYが禁じているのはコードの重複ではなく、「業務知識の重複」です。
コードが見た目に同じでも、それぞれが独立した業務ルールを実装しているなら、共通化すべきではありません。「たまたまコードが一致している状態」と「同じ知識を二重管理している状態」は異なります。
逆に言えば、由来がひとつしかない事実は、共通化すべきです。法律で定まった消費税率のように「誰がいつ見ても同じ値であるべきもの」は、複数箇所に書き散らすと改正のたびに修正漏れが起きます。避けるべきなのは、由来の異なるルールを「見た目が同じ」というだけでひとつにまとめることです。
たとえば、受注確認画面と請求書発行の両方に「消費税10%を加算する」処理があるとします。ここで共通化すべきなのは「税率が10%である」という知識です。これは法律で定まった一つの事実なので、定数として一箇所にまとめるべきです。一方、その税率を「いつ・どの単位で丸めるか」は文脈によって異なります。請求書はインボイス制度(適格請求書等保存方式)の対象であり、一つの請求書につき税率ごとに1回だけ端数処理を行うという法的なルールに従う必要があります。複数の明細行がある場合は、行ごとの金額を合計してから一度だけ丸めなければなりません。一方、受注確認画面は社内向けの参考表示であり、行ごとに丸めても問題ありません。つまり税率という知識は共有しつつ、丸め方という業務ルールは分けて実装する必要があります。
// NG: 税率と丸め方を1つのメソッドに混在させている
public class AmountCalculator {
// 受注確認画面・請求書の両方から呼ばれる
public BigDecimal applyTax(BigDecimal price) {
return price.multiply(new BigDecimal("1.10"))
.setScale(0, RoundingMode.HALF_UP);
}
}
// OK: 税率という知識は共有し、丸め方(業務ルール)は分ける
public final class TaxRate {
public static final BigDecimal STANDARD = new BigDecimal("0.10");
}
public class OrderAmountPolicy {
// 受注確認画面:行ごとに端数処理してよい
public BigDecimal applyTax(BigDecimal price) {
return price.multiply(BigDecimal.ONE.add(TaxRate.STANDARD))
.setScale(0, RoundingMode.HALF_UP);
}
}
public class InvoiceAmountPolicy {
// 請求書:インボイス制度により、税率ごとに1回だけ端数処理する
public BigDecimal applyTax(List<BigDecimal> lineTotals) {
BigDecimal subtotal = lineTotals.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
return subtotal.multiply(BigDecimal.ONE.add(TaxRate.STANDARD))
.setScale(0, RoundingMode.HALF_UP);
}
}
OKの例では、税率という共通の知識はTaxRateにまとめつつ、丸め方は用途ごとに分けています。一見似ている処理でも、法律上の制約が異なれば、引数や集計の単位そのものが変わっていきます。最初から1つのメソッドにまとめてしまうと、この違いを表現できません。
共通化を見送る、二つの異なる理由
共通化を見送る判断には、性質の異なる二つの理由があります。混同すると、なぜコピペを許容しているのか分からなくなるため、整理しておきます。
一つは、消費税の丸め方の例のように「適用ルールが業務知識として明確に別物だと判断できる」場合です。この場合、税率という値そのものは共有します。一方で、丸め方という適用ロジックは共通化しません。出現回数に関係なく、最初から別々に実装しておきます。
もう一つは、「まだ判断がつかない」場合です。同じようなコードが2回登場した段階では、そのロジックが将来どう変化するか、どのアクターに依存するかを正しく予見するのは困難です。このようなときは「Rule of Three(3回の法則)」が有効です。3回目、あるいは同じ「知識」が登場するまでは、あえてコピペを許容します。3箇所、4箇所と重複が増えてきたとき、初めて「これは本当に共通の知識だ」という境界線が見えてきます。
いずれの理由であっても、不適切な共通化による密結合よりは、数行の重複コードの方がはるかにマシです。
第2章:横断的処理はSpring Bootの機能で共通化する
ログ出力・認証・認可・例外処理のように、業務ドメインに依存しないシステム全体の関心事を「横断的処理」と呼びます。これらは自前のユーティリティを作るのではなく、フレームワークが提供する仕組みに乗って実装するのが効率的です。
「自前でコードを書かない」ことも、立派な共通化のテクニックです。Spring Bootには、よくある業務要件をスマートに解決する仕組みが備わっています。
代表例がカスタムバリデーションです。「郵便番号の形式チェック」を共通化したい場合、Util.isValidZipCode(String code) というメソッドを作るのではなく、独自のバリデーションアノテーションを作成します。
// アノテーション定義
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ZipCodeValidator.class)
public @interface ZipCode {
String message() default "郵便番号の形式が正しくありません";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// バリデーター実装
public class ZipCodeValidator implements ConstraintValidator<ZipCode, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true;
return value.matches("\\d{3}-\\d{4}");
}
}
// 利用側:フィールドへの付与だけで完結する
public class AddressRequest {
@ZipCode
private String zipCode;
}
業務ルールが「コード(命令)」ではなく「メタデータ(宣言)」として記述されるため、コードの可読性が高まります。自前の Util クラスを増やす前に、「Springの標準機能で解決できないか」と一歩立ち止まる癖をつけましょう。
第3章:まとめ ── 判断フレームワークと置き場所
第1章・第2章を踏まえ、共通化するかどうか、するならどこに置くかを判断するための3つの問いを整理します。
① 名前空間(具体的な名前を付けられるか?)
CommonLogic や AppUtil といった名前に逃げそうになったら、共通化を見送るサインです(第1章)。「誰の、何のための処理か」が説明できないものは、異なる理由で変更されるロジックの寄せ集めになります。
② 利用者のスコープ(全システム横断か、特定ドメイン内のみか?)
全システム共通の汎用処理(例:日付変換)と、特定ドメイン内の業務ルール(例:消費税の端数処理)を区別します。
③ 技術的依存度(純粋なJavaコードか、Springに依存するか?)
純粋なJavaコード(POJO)か、Springのコンテキストに依存する処理(第2章)かで、使えるレイヤーが絞られます。
①をクリアできない場合は共通化を見送ります。①をクリアできたら、②と③の回答をもとに、以下の表から置き場所を選んでください。Filter・Interceptor・AOP・ControllerAdviceは横断的処理の受け皿です。Service・Component・Domain Modelはビジネスロジックの置き場所です。自前の共通クラスを作る前に、フレームワークが提供する適切なレイヤーに処理を委譲するのが鉄則です。
| 置き場所 | 役割・特徴 | 具体例 |
|---|---|---|
| Filter | サーブレットコンテナレベルのインフラ的関心事。DispatcherServlet到達前にフックし、Spring Beanには直接アクセスできない。 | 文字エンコーディング、CORS設定、圧縮、Spring Securityのフィルタチェーンによる大枠の認証・認可 |
| Interceptor | Spring MVCレベルの横断的関心事。ハンドラー(コントローラーメソッド)の情報にアクセスでき、Spring Beanを注入できる。 | ハンドラーのアノテーションに応じたログ出力、画面遷移前後の共通処理 |
| AOP(Aspect) | 業務ロジックに依存しない横断処理。メソッドの前後・例外発生時に処理を織り込む。 |
@Transactional、@Cacheable(マスタデータ等のキャッシュ)、@PreAuthorizeによる細粒度の認可、パフォーマンス計測、リトライ処理 |
| ControllerAdvice | プレゼンテーション層の例外集約。アプリ全体の例外ハンドリングを一点に集中させる。 |
try-catchの一掃、共通エラーレスポンス(JSON)の生成 |
| Service(Shared) | 複数ドメインに跨る純粋な業務知識。特定の画面に依存せず、複数のユースケースから呼ばれる。 | 税金計算、割引ルール |
| Component(Utility) | 業務知識を含まない純粋な技術的処理。ドメインの文脈を持たない。 | 独自の日付計算、文字列変換、暗号化・復号、ファイルI/O |
| Domain Model | データの状態(データ)と振る舞い(ロジック)をカプセル化し、不正な状態を防ぐ。 | エンティティ生成時のバリデーション、ステータス遷移ルール |
参考文献
Qiita @Gaakuuさま (2026年3月23日)「同じ処理=共通化ではダメ?共通化の罠」
https://qiita.com/Gaakuu/items/08449740998cff7469df
2026年7月11日アクセス.
Zenn michiharuさま (2024年7月31日)「コードの共通化について語るときに僕の語ること」
https://zenn.dev/michiharu/articles/15c6802ae3c5f4
2026年7月11日アクセス.
はてなブログ susisuさま (2022年7月3日)「共通化すれば良いとは限らない」
https://susisu.hatenablog.com/entry/2022/07/03/200715
2026年7月11日アクセス.