5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

こんにちは!
エンジニアの matsuura です。

みなさん、ソフトウェア開発していますか?
昨今、ソフトウェア開発において、ビジネス要件の複雑化と技術スタックの多様化が進んでいます。
このような環境では、コアビジネスロジックを技術的な詳細から守ることが、持続可能な開発を実現する鍵となります。

例えば、ECサイトで「在庫がある商品のみ注文可能」というビジネスルールがあるとします。
このルールが、「コアビジネスロジック」に該当します。
ここで、もし技術的な変化、例えば、データベースがMySQLからPostgreSQLに変わったり、フロントエンドがReactからVueに変わったりしたとしても、本質的には変わらないはずです。

今回紹介するオニオンアーキテクチャは、この「本質的なビジネスロジック」を中心に据え、依存関係逆転の原理で技術的な詳細から守るアーキテクチャパターンです。

オニオンアーキテクチャについて

従来のレイヤードアーキテクチャの罠

従来のレイヤードアーキテクチャでは、上位層が下位層に依存する「溝落ち型」の構造を持っています。

layered-architecture.png

この構造の「依存関係」とは、上位層が下位層の具体的な実装詳細に結びついている状態です。
例えば、Service層が「user_name」や「user_id」といったDBカラム名を直接知っているような状態です。

このおかげで、下層の変更が「ドミノ倒し」のように上層まで波及してしまう問題を有しているのです。

で、何が問題なのか?

実際にありそうなシナリオを例にレイヤードアーキテクチャの問題点を解説します。

シナリオ1: データベースの技術変更

プロジェクトの途中で、パフォーマンスやコストの理由でMySQLからPostgreSQL、さらにはMongoDBへの移行が決定しました。

従来のアーキテクチャでは…

  1. テーブル設計の大幅変更
  2. EntityクラスのSQL固有の気遣いを修正
  3. Service層のEntity依存部分を全て書き直し
  4. ビジネスロジックのテストを全てやり直し

シナリオ2: 外部APIのサービス変更

決済サービスをStripeからPayPalに変更したいのですが、APIのResponse形式が全く違う。

従来のアーキテクチャでは…

  • 決済処理のコアロジックが外部APIのデータ構造に依存
  • API変更でビジネスルールまで影響を受ける

オニオンアーキテクチャは、こうした「技術の波及」を防ぐための防波堤を構築します。

オニオンアーキテクチャとは

onion-architecture.png

依存関係逆転の原理

オニオンアーキテクチャの核心は、DomainとInfrastructureの依存関係を逆転させることです!
え!?それだけ?
それだけです。

レイヤードアーキテクチャとの違いをより具体的に列挙するならば、以下の通りとなります。

  • Domain層で、Repositoryインターフェースを定義する
  • Infrastructure内で、その実装クラスを定義する
  • DomainはDBに依存しない設計にする

この設計により、DBの変更があってもDomain層は影響を受けません。

ビジネスを中心に据えた「防波堤」の作り方

Infrastructure層の最大の使命は「技術的な詳細を完全に防波堤の向こうに閉じ込める」ことです。

具体的な防波方法:

  • Infrastructure層において、user_nmcreated_atといったDB固有の命名を他の層に漏らさない
  • DomainModelではuser_nmというDB固有の命名を使用せず、Userクラスのnameプロパティとして表現する
  • Infrastructure層が「翻訳器」となり、DB用語とビジネス用語を相互変換する
    • 例) DBのuser_nmカラム ⇔ DomainModelのUser.nameプロパティ

こうすることで、DBが変わってもビジネスロジック(Domain層)は1行も変更しなくて済む理想的な状態を作り出せます。

各層の詳細説明

Domain Model層

アプリケーションのビジネスドメインを表現する層です。

  • 特徴
    • モノ・ヒト・コトに分類されるビジネス概念を表現
    • データベースのテーブル定義に依存しない
    • Entityはあくまでテーブル定義であり、Domain Modelとは別物

Domain Service層

ビジネスロジックを記述する層です。

  • 特徴
    • ビジネスルールの実装
    • Repositoryインターフェースの定義
    • ドメイン固有の処理ロジック

Application Service層

ユーザーの行動(ユースケース)を表現する層です。

  • 特徴
    • 各ユースケースごとにクラスを作成
    • Domain Serviceのインターフェースを順次呼び出し
    • ビジネスロジックは含まず、オーケストレーションに専念

Infrastructure層

外部システムとの連携を担当する層です。

  • 特徴
    • Repositoryの実装クラス
    • 外部API、データベースへのアクセス
    • 外部システムの影響を他層に波及させない

Presentation層

APIのリクエスト・レスポンスに責任を持つ層です。

  • 特徴
    • HTTPリクエストを受け付る
    • レスポンスの生成する
    • 入力値の検証とDomainModelへの変換する

実装例とディレクトリ構成

ECサイトの商品管理でのRepository実装例

// Domain Model層: 商品の情報を表現
public class Product {
    private final ProductId id;
    private final ProductName name;
    private final Price price;
    private final Stock stock;

    public Product(ProductId id, ProductName name, Price price, Stock stock) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.stock = stock;
    }

    // ビジネスロジック: 在庫チェック
    public boolean isAvailable(int requestedQuantity) {
        return stock.getValue() >= requestedQuantity;
    }

    // (省略)
}

// Value Object: 商品ID
public class ProductId {
    private final String value;

    public ProductId(String value) {
        if (value == null || value.isEmpty()) {
            throw new IllegalArgumentException("Product ID cannot be empty");
        }
        this.value = value;
    }

    public String getValue() { return value; }

    @Override
    public boolean equals(Object object) {
        if (this == object) return true;
        if (!(object instanceof ProductId)) return false;
        ProductId productId = (ProductId) object;
        return Objects.equals(value, productId.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}

// Domain Service層: Repositoryインターフェース
public interface ProductRepository {
    /**
     * 商品IDで商品を検索
     * プリミティブ型ではなくValue Objectを使用
     */
    Optional<Product> findById(ProductId productId);

    /**
     * カテゴリ別商品検索
     * ページングも考慮した設計
     */
    List<Product> findByCategory(Category category, Pageable pageable);

    /**
     * 商品の保存
     * 新規登録・更新を区別しない
     */
    void save(Product product);

    /**
     * 在庫が少ない商品を取得
     * ビジネスルールを含んだクエリ
     */
    List<Product> findLowStockProducts(int threshold);
}

Infrastructure層でのデータ変換の実装例

// Infrastructure層: DB固有の型からドメインモデルへの変換
@Repository
public class JpaProductRepository implements ProductRepository {

    @Autowired
    private ProductJpaRepository jpaRepository;

    @Override
    public Optional<Product> findById(ProductId productId) {
        // 1. DB固有の型でクエリ実行
        Optional<ProductEntity> entity = jpaRepository.findById(productId.getValue());

        // 2. DBエンティティをドメインモデルに変換
        return entity.map(this::toDomainModel);
    }

    @Override
    public void save(Product product) {
        // 1. ドメインモデルをDBエンティティに変換
        ProductEntity entity = toEntity(product);

        // 2. DB固有の型で保存
        jpaRepository.save(entity);
    }

    // ドメインモデル → DBエンティティ変換
    private ProductEntity toEntity(Product product) {
        ProductEntity entity = new ProductEntity();
        entity.setId(product.getId().getValue());
        entity.setName(product.getName().getValue());
        entity.setPrice(product.getPrice().getAmount());
        entity.setStock(product.getStock().getValue());
        return entity;
    }

    // DBエンティティ → ドメインモデル変換
    private Product toDomainModel(ProductEntity entity) {
        return new Product(
            new ProductId(entity.getId()),
            new ProductName(entity.getName()),
            new Price(entity.getPrice()),
            new Stock(entity.getStock())
        );
    }
}

このようにすることで、Infrastructure層がDBの詳細を完全に隠蔽し、他の層にはドメインモデルのみを公開することで、データベース変更の影響を局所化できます。

ECサイトでのプロジェクト構造例

┳ app
┃  ┣ presentation          # UI層・API層
┃  ┃  ┣ controllers        # RESTコントローラー
┃  ┃  ┃  ┣ ProductController.java
┃  ┃  ┃  ┗ OrderController.java
┃  ┃  ┣ dto                # データ転送オブジェクト
┃  ┃  ┃  ┣ ProductResponse.java
┃  ┃  ┃  ┗ CreateOrderRequest.java
┃  ┃  ┗ handlers           # エラーハンドリング
┃  ┣ application           # アプリケーション層
┃  ┃  ┗ usecase            # ユースケース実装
┃  ┃     ┣ ProductApplicationUseCase.java
┃  ┃     ┗ OrderApplicationUseCase.java
┃  ┣ domain                # ドメイン層(コア)
┃  ┃  ┣ models             # エンティティ・集約
┃  ┃  ┃  ┣ product         # 商品集約
┃  ┃  ┃  ┃  ┣ Product.java
┃  ┃  ┃  ┃  ┣ ProductId.java  (Value Object)
┃  ┃  ┃  ┃  ┣ ProductName.java
┃  ┃  ┃  ┃  ┣ Price.java
┃  ┃  ┃  ┃  ┗ Stock.java
┃  ┃  ┃  ┣ order           # 注文集約
┃  ┃  ┃  ┃  ┣ Order.java
┃  ┃  ┃  ┃  ┣ OrderId.java
┃  ┃  ┃  ┃  ┣ OrderItem.java
┃  ┃  ┃  ┃  ┗ OrderStatus.java
┃  ┃  ┃  ┗ customer        # 顧客集約
┃  ┃  ┃     ┣ Customer.java
┃  ┃  ┃     ┗ CustomerId.java
┃  ┃  ┣ services           # ドメインサービス
┃  ┃  ┃  ┣ PricingService.java
┃  ┃  ┃  ┗ InventoryService.java
┃  ┃  ┗ repositories       # リポジトリインターフェース
┃  ┃     ┣ ProductRepository.java
┃  ┃     ┣ OrderRepository.java
┃  ┃     ┗ CustomerRepository.java
┃  ┗ infrastructure        # インフラストラクチャ層
┃     ┣ entities           # DBエンティティ(テーブル定義)
┃     ┃  ┣ ProductEntity.java
┃     ┃  ┗ OrderEntity.java
┃     ┣ repositories       # Repository実装
┃     ┃  ┣ ProductRepositoryImpl.java
┃     ┃  ┣ OrderRepositoryImpl.java
┃     ┃  ┗ CustomerRepositoryImpl.java
┃     ┣ mappers            # ドメインモデル⇔DBエンティティ変換
┃     ┃  ┣ ProductMapper.java
┃     ┃  ┗ OrderMapper.java
┃     ┗ external           # 外部API連携
┃        ┣ payment         # 決済サービス
┃        ┗ notification    # 通知サービス

おまけ

※ この項目はClaude(Sonnet 4)により生成してもらった項目になります。

オニオンアーキテクチャのメリット

  1. 高い保守性: データベースの変更がビジネスロジックに影響しない
  2. テスタビリティ: インターフェースによりモックが容易
  3. 関心の分離: 各層の責務が明確
  4. 拡張性: 新しい外部システムの追加が容易

導入時のハードルと対策

1. チームの学習コスト

  • 初回は「なぜこんなに面倒なんだ?」という反応が一般的
  • 対策: 小さな機能から始めて、メリットを体感してもらう

2. 初期のオーバーエンジニアリングリスク

  • 3ヶ月で終わるプロトタイプにはオーバースペック
  • 判断基準: 「1年以上運用」「3人以上の開発者」が目安

3. レイヤー構造の認知負荷

  • 最初は「どのファイルを修正すればいい?」が分からない
  • 対策: 明確なディレクトリ構造とネーミング規約で解決

オニオンアーキテクチャを「使うべきか?」の判断フローチャート

以下の質問に「YES」が2個以上あるなら、オニオンアーキテクチャの導入を検討してみてください:

  • 「1年以上継続運用する予定」
  • 「開発メンバーが3人以上」
  • 「データベースや外部APIの変更が予想される」
  • 「ビジネスルールが10個以上ある」
  • 「ユニットテストの保守性を重視したい」
  • 「異なるチームが同じコードベースを触る」

逆に、以下の場合はシンプルなアーキテクチャで十分:

  • MVPやプロトタイプの段階
  • シンプルなCRUDアプリケーション
  • 個人開発の小規模プロジェクト

オニオンアーキテクチャは 「ビジネスロジックを技術から守る」 ための強力な防波堤です。依存関係逆転のシンプルな原理を理解し、段階的に導入することで、「変更に強いシステム」が構築できます。

最初は時間がかかるかもしれませんが、長期的には「ビジネスロジックの修正がスムーズ」「テストが書きやすい」「新人がコードを理解しやすい」といった大きなメリットを実感できるはずです。

最後に

エンジニア募集

Gakken LEAP では教育をアップデートしていきたいエンジニアを絶賛大募集しています!!
ぜひお気軽にカジュアル面談へお越しください!!

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?