【アーキテクチャ】モジュラモノリス - j-komatsu/myCheatSheet GitHub Wiki

【アーキテクチャ】モジュラモノリス

📋 概要(What)

モジュラモノリス(Modular Monolith) は、1つのプロセス内で複数の独立したモジュールを持つアーキテクチャパターンです。

アーキテクチャ デプロイ単位 モジュール分離 通信方式
モノリス 1つ なし(密結合) メソッド呼び出し
モジュラモノリス 1つ 明確な境界 インターフェース経由
マイクロサービス 複数 プロセス分離 HTTP/gRPC/メッセージング

キーコンセプト

  • 1プロセス内での独立モジュール化:各モジュールは明確な境界を持つ
  • DDD(ドメイン駆動設計)との親和性:Bounded Contextをモジュール単位で表現
  • 段階的移行:モノリス → モジュラモノリス → マイクロサービスへの自然な進化パス

⚖️ メリット・デメリット(Why)

比較表

観点 モノリス モジュラモノリス マイクロサービス
開発速度 初期◎ / 中期△ 初期△ / 中期◎
デプロイ複雑性
チーム独立性 ×
テスト容易性
運用コスト
スケーラビリティ
技術スタック統一 必須 必須 自由

適用シーン

モジュラモノリスが最適

  • チーム規模:5〜20人程度
  • ドメイン複雑度:中〜高
  • デプロイ要件:週次〜日次デプロイ
  • インフラ習熟度:中程度

⚠️ マイクロサービスを検討すべき

  • チーム規模:20人以上
  • 独立デプロイ要件:各チームが1日複数回デプロイ
  • スケール要件:一部機能のみ大規模スケールが必要

🏗️ 実装構造(How)

プロジェクト構成例(Spring Boot Multi-Module)

my-application/
├── order-module/
│   ├── domain/
│   │   ├── Order.java
│   │   └── OrderRepository.java (interface)
│   ├── application/
│   │   └── OrderService.java
│   └── infrastructure/
│       └── JpaOrderRepository.java
├── payment-module/
│   ├── domain/
│   │   ├── Payment.java
│   │   └── PaymentRepository.java
│   ├── application/
│   │   └── PaymentService.java
│   └── infrastructure/
│       └── JpaPaymentRepository.java
├── api/
│   ├── OrderController.java
│   └── PaymentController.java
└── build.gradle

build.gradle(Multi-Module構成)

// ルートプロジェクト
plugins {
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
}

subprojects {
    apply plugin: 'java'
    apply plugin: 'org.springframework.boot'

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter'
    }
}

// settings.gradle
include 'order-module'
include 'payment-module'
include 'api'

モジュール間依存の制御

❌ 悪い例:直接依存

// PaymentService.java
public class PaymentService {
    @Autowired
    private OrderRepository orderRepository; // 他モジュールの実装に直接依存
}

✅ 良い例:インターフェース経由

// order-module/domain/OrderQuery.java (公開インターフェース)
public interface OrderQuery {
    Optional<Order> findById(Long orderId);
}

// payment-module/application/PaymentService.java
public class PaymentService {
    private final OrderQuery orderQuery; // インターフェースに依存

    public PaymentService(OrderQuery orderQuery) {
        this.orderQuery = orderQuery;
    }
}

Java 9+ Module System(module-info.java)

// order-module/module-info.java
module order.module {
    requires spring.context;
    requires spring.data.jpa;

    exports com.example.order.domain;      // 公開パッケージ
    exports com.example.order.application; // 公開パッケージ

    // infrastructure層は非公開(外部から直接アクセス不可)
}

📊 アーキテクチャ図

graph LR
    A[Traditional Monolith] -->|Refactor| B[Modular Monolith]
    B -->|Extract| C[Microservices]

    subgraph "Modular Monolith"
        B1[Order Module]
        B2[Payment Module]
        B3[User Module]
        B1 -.->|interface| B2
        B2 -.->|interface| B3
    end

    subgraph "Microservices"
        C1[Order Service]
        C2[Payment Service]
        C3[User Service]
        C1 -->|HTTP/gRPC| C2
        C2 -->|HTTP/gRPC| C3
    end
Loading

モジュール内部構造(クリーンアーキテクチャ)

graph TD
    A[Controller/API Layer] --> B[Application Layer]
    B --> C[Domain Layer]
    C --> D[Infrastructure Layer]

    D -.->|implements| C

    style C fill:#f9f,stroke:#333,stroke-width:4px
    style D fill:#bbf,stroke:#333,stroke-width:2px
Loading

💡 実装例(Spring Boot)

モジュール単位でのDI設定

// order-module/config/OrderModuleConfig.java
@Configuration
@ComponentScan(basePackages = "com.example.order")
public class OrderModuleConfig {

    @Bean
    public OrderService orderService(OrderRepository orderRepository) {
        return new OrderService(orderRepository);
    }

    @Bean
    public OrderQuery orderQuery(OrderRepository orderRepository) {
        return new OrderQueryImpl(orderRepository); // 公開インターフェースの実装
    }
}

ドメインモデル例

// order-module/domain/Order.java
@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String customerId;
    private BigDecimal totalAmount;
    private OrderStatus status;

    // ビジネスロジックはドメイン層に集約
    public void confirm() {
        if (this.status != OrderStatus.PENDING) {
            throw new IllegalStateException("Order must be in PENDING status");
        }
        this.status = OrderStatus.CONFIRMED;
    }
}

Application Service(ユースケース層)

// order-module/application/OrderService.java
@Service
@Transactional
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentClient paymentClient; // 外部モジュールとの連携

    public OrderService(OrderRepository orderRepository, PaymentClient paymentClient) {
        this.orderRepository = orderRepository;
        this.paymentClient = paymentClient;
    }

    public void placeOrder(CreateOrderCommand command) {
        Order order = new Order(command.getCustomerId(), command.getItems());
        orderRepository.save(order);

        // 他モジュールとの連携はインターフェース経由
        paymentClient.initiatePayment(order.getId(), order.getTotalAmount());
    }
}

🔄 他アーキテクチャとの関係

アーキテクチャパターン モジュラモノリスとの関係
クリーンアーキテクチャ 各モジュール内部で適用可能
レイヤードアーキテクチャ モジュール内の基本構造として使用
ヘキサゴナルアーキテクチャ Port/Adapter パターンをモジュール境界に適用
マイクロサービス モジュラモノリスからの段階的移行先
CQRS モジュール内でコマンド/クエリ分離可能

移行戦略

Phase 1: レガシーモノリス
  └─ すべてが密結合、レイヤ構造も曖昧

Phase 2: モジュラモノリス化
  └─ ドメイン境界でモジュール分割
  └─ インターフェース経由での依存関係明確化
  └─ モジュール単位でのテスト可能

Phase 3: マイクロサービス化(必要に応じて)
  └─ 独立デプロイが必要なモジュールから順次分離
  └─ プロセス間通信への変更(同期/非同期)

⚠️ よくある課題と対策

1. モジュール間依存が密結合化

問題

// payment-module が order-module の実装クラスに直接依存
import com.example.order.infrastructure.JpaOrderRepository;

解決策

  • 公開インターフェースのみをexportsで公開
  • 依存逆転の原則(DIP)を適用
  • ArchUnitなどで依存関係をテストで検証
@ArchTest
static final ArchRule moduleDependencyRule =
    noClasses()
        .that().resideInPackage("..payment..")
        .should().dependOnClassesThat().resideInPackage("..order.infrastructure..");

2. 共通ユーティリティ層の膨張

問題

  • common-utilsモジュールが肥大化し、すべてのモジュールが依存

解決策

  • 真に共通的なもの(DateUtil等)のみ配置
  • ドメイン固有のロジックは各モジュール内に
  • Shared Kernelパターン(DDD)を最小限に適用

3. 循環依存の発生

問題

Order Module → Payment Module → User Module → Order Module

解決策

  • イベント駆動アーキテクチャの導入(Spring ApplicationEvent)
  • 共通インターフェースを別モジュールに分離
  • 依存方向を一方向に統一(上位モジュール → 下位モジュール)
// order-module内でイベント発行
@Service
public class OrderService {
    private final ApplicationEventPublisher eventPublisher;

    public void confirmOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.confirm();

        // 他モジュールに直接依存せず、イベントで通知
        eventPublisher.publishEvent(new OrderConfirmedEvent(orderId));
    }
}

// payment-module内でイベント受信
@Component
public class OrderEventListener {
    @EventListener
    public void handleOrderConfirmed(OrderConfirmedEvent event) {
        // 支払い処理を開始
    }
}

4. 統合テストの難易度

解決策

// モジュール単位でのテスト
@SpringBootTest(classes = OrderModuleConfig.class)
class OrderServiceTest {
    @Autowired
    private OrderService orderService;

    @MockBean
    private PaymentClient paymentClient; // 外部モジュールはモック化

    @Test
    void testPlaceOrder() {
        // テスト実装
    }
}

✅ まとめとベストプラクティス

重要な原則

  1. 早すぎるマイクロサービス化を避ける

    • 最初からマイクロサービスにすると運用コストが高い
    • まずはモジュラモノリスで境界を明確にする
  2. モジュール境界 = Bounded Context

    • DDDの概念を活用し、ビジネスドメイン単位で分割
    • ユビキタス言語をモジュール内で統一
  3. 依存方向の徹底管理

    • Domain層は他の何にも依存しない
    • Infrastructure層がDomain層の抽象に依存(依存性逆転)
  4. 段階的な進化

    モノリス → モジュラモノリス → (必要なら)マイクロサービス
    

チームスケール別の推奨アプローチ

チーム規模 推奨アーキテクチャ
1〜5人 シンプルなモノリス
5〜20人 モジュラモノリス
20人以上 マイクロサービスも検討

📚 付録

用語集

  • Bounded Context(境界付けられたコンテキスト): DDDにおけるモデルの適用範囲
  • Aggregate(集約): ドメインオブジェクトの整合性境界
  • Module Boundary(モジュール境界): モジュール間の依存関係を制御する境界
  • Port/Adapter: ヘキサゴナルアーキテクチャのパターン(外部との接点を抽象化)

推奨参考資料

練習課題

課題1: 既存のシンプルなECサイト(モノリス)を以下の3モジュールに分割してみましょう

  • product-module(商品カタログ)
  • order-module(注文処理)
  • customer-module(顧客管理)

課題2: モジュール間の循環依存が発生した場合、イベント駆動で解決する実装例を作成してみましょう


🔗 次に読むべき関連トピック

  • 【アーキテクチャ】マイクロサービスアーキテクチャ - モジュラモノリスからの移行先
  • 【アーキテクチャ】イベント駆動アーキテクチャ - モジュール間疎結合の実現手法
  • 【アーキテクチャ】CQRS(コマンドクエリ責任分離) - モジュール内での適用パターン
  • 【アーキテクチャ】ドメイン駆動設計(DDD) - モジュール境界の設計理論
  • 【アーキテクチャ】クリーンアーキテクチャ - モジュール内部の構造設計

📝 最終更新: 2025-10-25

⚠️ **GitHub.com Fallback** ⚠️