【アーキテクチャ】ヘキサゴナルアーキテクチャ(ポート&アダプター) - j-komatsu/myCheatSheet GitHub Wiki

【アーキテクチャ】ヘキサゴナルアーキテクチャ(ポート&アダプター)

📋 概要(What)

ヘキサゴナルアーキテクチャ(Hexagonal Architecture) は、ビジネスロジックを外部依存から完全に分離し、テスタビリティと保守性を高めるアーキテクチャパターンです。別名「ポート&アダプターパターン」とも呼ばれます。

基本概念

graph TD
    subgraph "外部世界(Outside)"
        UI[Web UI]
        REST[REST API]
        CLI[CLI]
        DB[(Database)]
        EXT[External API]
        MQ[Message Queue]
    end

    subgraph "ヘキサゴン(Hexagon)"
        CORE[ビジネスロジック<br/>Domain Model]
    end

    subgraph "アダプター層"
        WA[Web Adapter]
        RA[REST Adapter]
        CA[CLI Adapter]
        DBA[DB Adapter]
        EXTA[API Adapter]
        MQA[MQ Adapter]
    end

    UI --> WA
    REST --> RA
    CLI --> CA
    WA --> CORE
    RA --> CORE
    CA --> CORE
    CORE --> DBA
    CORE --> EXTA
    CORE --> MQA
    DBA --> DB
    EXTA --> EXT
    MQA --> MQ

    style CORE fill:#f9f,stroke:#333,stroke-width:4px
Loading
概念 説明
ヘキサゴン(六角形) ビジネスロジックのコア。外部依存なし
ポート(Port) インターフェース。内部と外部の契約
アダプター(Adapter) ポートの具体的な実装。外部技術と接続
プライマリポート アプリケーションを駆動する入口(API等)
セカンダリポート アプリケーションが利用する出口(DB等)

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

メリット

メリット 詳細
テスタビリティ 外部依存をモック化しやすい
技術の差し替え可能性 DBやフレームワークを容易に変更可能
ビジネスロジックの独立性 フレームワークに依存しない純粋なドメインモデル
複数のUI対応 Web、CLI、APIなど複数インターフェース共存可能
保守性の向上 関心の分離が明確

デメリット・課題

課題 対策
初期コスト 小規模プロジェクトでは過剰設計に注意
インターフェース数の増加 適切な粒度で設計
学習コスト チーム全体での理解が必要
ボイラープレートコード IDEのコード生成機能を活用

🏗️ レイヤー構造(How)

基本構造

src/
├── domain/                    # ヘキサゴンのコア
│   ├── model/                 # ドメインモデル
│   │   ├── Order.java
│   │   └── OrderItem.java
│   ├── service/               # ドメインサービス
│   │   └── OrderService.java
│   └── port/                  # ポート(インターフェース)
│       ├── in/                # プライマリポート(入力)
│       │   ├── CreateOrderUseCase.java
│       │   └── GetOrderUseCase.java
│       └── out/               # セカンダリポート(出力)
│           ├── OrderRepository.java
│           ├── PaymentGateway.java
│           └── NotificationService.java
│
├── adapter/                   # アダプター層
│   ├── in/                    # 入力アダプター
│   │   ├── web/
│   │   │   └── OrderController.java
│   │   ├── cli/
│   │   │   └── OrderCli.java
│   │   └── messaging/
│   │       └── OrderEventListener.java
│   └── out/                   # 出力アダプター
│       ├── persistence/
│       │   ├── JpaOrderRepository.java
│       │   └── OrderEntity.java
│       ├── payment/
│       │   └── StripePaymentAdapter.java
│       └── notification/
│           └── EmailNotificationAdapter.java
│
└── config/                    # 依存性注入設定
    └── ApplicationConfig.java

💡 実装例(Spring Boot)

1. ドメイン層(ヘキサゴンのコア)

ドメインモデル

// domain/model/Order.java
public class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private final List<OrderItem> items;
    private OrderStatus status;
    private final Money totalAmount;

    public Order(CustomerId customerId, List<OrderItem> items) {
        this.id = OrderId.generate();
        this.customerId = customerId;
        this.items = List.copyOf(items);
        this.status = OrderStatus.PENDING;
        this.totalAmount = calculateTotal(items);
    }

    // ビジネスロジック
    public void confirm() {
        if (this.status != OrderStatus.PENDING) {
            throw new IllegalStateException("Order can only be confirmed when pending");
        }
        this.status = OrderStatus.CONFIRMED;
    }

    public void cancel(String reason) {
        if (this.status == OrderStatus.SHIPPED) {
            throw new IllegalStateException("Cannot cancel shipped order");
        }
        this.status = OrderStatus.CANCELLED;
    }

    private Money calculateTotal(List<OrderItem> items) {
        return items.stream()
            .map(OrderItem::getSubtotal)
            .reduce(Money.ZERO, Money::add);
    }

    // getters...
}

プライマリポート(入力ポート = ユースケース)

// domain/port/in/CreateOrderUseCase.java
public interface CreateOrderUseCase {
    OrderId createOrder(CreateOrderCommand command);
}

// domain/port/in/CreateOrderCommand.java
public record CreateOrderCommand(
    CustomerId customerId,
    List<OrderItemDto> items,
    ShippingAddress shippingAddress
) {}

セカンダリポート(出力ポート)

// domain/port/out/OrderRepository.java
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId orderId);
    List<Order> findByCustomerId(CustomerId customerId);
}

// domain/port/out/PaymentGateway.java
public interface PaymentGateway {
    PaymentResult processPayment(OrderId orderId, Money amount);
}

// domain/port/out/NotificationService.java
public interface NotificationService {
    void sendOrderConfirmation(Order order);
}

2. ドメインサービス(ユースケース実装)

// domain/service/OrderService.java
@Service
@Transactional
public class OrderService implements CreateOrderUseCase, ConfirmOrderUseCase {

    private final OrderRepository orderRepository;
    private final PaymentGateway paymentGateway;
    private final NotificationService notificationService;

    // コンストラクタインジェクション(ポートに依存)
    public OrderService(
        OrderRepository orderRepository,
        PaymentGateway paymentGateway,
        NotificationService notificationService
    ) {
        this.orderRepository = orderRepository;
        this.paymentGateway = paymentGateway;
        this.notificationService = notificationService;
    }

    @Override
    public OrderId createOrder(CreateOrderCommand command) {
        // ドメインモデルを生成
        List<OrderItem> items = command.items().stream()
            .map(dto -> new OrderItem(dto.productId(), dto.quantity(), dto.price()))
            .toList();

        Order order = new Order(command.customerId(), items);

        // 永続化(セカンダリポート経由)
        orderRepository.save(order);

        // 決済処理
        PaymentResult result = paymentGateway.processPayment(
            order.getId(),
            order.getTotalAmount()
        );

        if (result.isSuccess()) {
            order.confirm();
            orderRepository.save(order);
            notificationService.sendOrderConfirmation(order);
        }

        return order.getId();
    }
}

3. 入力アダプター(REST API)

// adapter/in/web/OrderController.java
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final CreateOrderUseCase createOrderUseCase;
    private final GetOrderUseCase getOrderUseCase;

    public OrderController(
        CreateOrderUseCase createOrderUseCase,
        GetOrderUseCase getOrderUseCase
    ) {
        this.createOrderUseCase = createOrderUseCase;
        this.getOrderUseCase = getOrderUseCase;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
        @RequestBody @Valid CreateOrderRequest request
    ) {
        // リクエストDTOをコマンドに変換
        CreateOrderCommand command = new CreateOrderCommand(
            new CustomerId(request.customerId()),
            request.items(),
            request.shippingAddress()
        );

        OrderId orderId = createOrderUseCase.createOrder(command);

        return ResponseEntity.status(HttpStatus.CREATED)
            .body(new OrderResponse(orderId.getValue()));
    }

    @GetMapping("/{orderId}")
    public ResponseEntity<OrderDetailResponse> getOrder(@PathVariable String orderId) {
        OrderView order = getOrderUseCase.getOrder(new OrderId(orderId));
        return ResponseEntity.ok(OrderDetailResponse.from(order));
    }
}

4. 出力アダプター(永続化)

// adapter/out/persistence/JpaOrderRepository.java
@Repository
public class JpaOrderRepository implements OrderRepository {

    private final SpringDataOrderRepository springDataRepo;
    private final OrderMapper mapper;

    @Override
    public void save(Order order) {
        OrderEntity entity = mapper.toEntity(order);
        springDataRepo.save(entity);
    }

    @Override
    public Optional<Order> findById(OrderId orderId) {
        return springDataRepo.findById(orderId.getValue())
            .map(mapper::toDomain);
    }

    @Override
    public List<Order> findByCustomerId(CustomerId customerId) {
        return springDataRepo.findByCustomerId(customerId.getValue()).stream()
            .map(mapper::toDomain)
            .toList();
    }
}

// JPA Entity(永続化の詳細)
@Entity
@Table(name = "orders")
class OrderEntity {
    @Id
    private String id;
    private String customerId;
    private String status;
    private BigDecimal totalAmount;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItemEntity> items;

    // getters/setters...
}

5. 出力アダプター(外部API連携)

// adapter/out/payment/StripePaymentAdapter.java
@Component
public class StripePaymentAdapter implements PaymentGateway {

    private final StripeClient stripeClient;

    @Override
    public PaymentResult processPayment(OrderId orderId, Money amount) {
        try {
            // Stripe APIを呼び出し
            PaymentIntent intent = stripeClient.createPaymentIntent(
                amount.getAmount(),
                amount.getCurrency()
            );

            return new PaymentResult(
                true,
                intent.getId(),
                "Payment processed successfully"
            );
        } catch (StripeException e) {
            return new PaymentResult(
                false,
                null,
                "Payment failed: " + e.getMessage()
            );
        }
    }
}

📊 依存関係の方向

重要な原則:依存性逆転(DIP)

graph LR
    subgraph "外部(Infrastructure)"
        WEB[Web Controller]
        JPA[JPA Repository]
    end

    subgraph "ポート(Interface)"
        IN[CreateOrderUseCase]
        OUT[OrderRepository]
    end

    subgraph "ドメイン層"
        SERVICE[OrderService]
    end

    WEB -->|depends on| IN
    SERVICE -.->|implements| IN
    SERVICE -->|depends on| OUT
    JPA -.->|implements| OUT

    style SERVICE fill:#f9f,stroke:#333,stroke-width:4px
Loading

ポイント:

  • ✅ 外部層(Adapter)はドメイン層(Port)に依存
  • ✅ ドメイン層は外部に依存しない
  • ✅ すべての依存が「内向き」

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

アーキテクチャ ヘキサゴナルとの関係
クリーンアーキテクチャ 同じ思想。クリーンは円形、ヘキサゴナルは六角形で表現
レイヤードアーキテクチャ ヘキサゴナルのほうが依存性逆転を明確化
オニオンアーキテクチャ ヘキサゴナルとほぼ同義
モジュラモノリス 各モジュール内部でヘキサゴナルを適用可能
DDD ドメインモデルの設計思想を共有

⚠️ よくある課題と対策

1. ポートとアダプターの粒度

問題

  • インターフェースが多すぎて管理が大変

解決策

// 粒度を適切に保つ
// ❌ 細かすぎる
interface SaveOrder {}
interface FindOrder {}
interface UpdateOrder {}

// ✅ 適切な粒度
interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId id);
    void delete(OrderId id);
}

2. ドメインモデルとエンティティの変換

問題

  • マッピングコードが冗長

解決策

// MapStructを活用
@Mapper(componentModel = "spring")
interface OrderMapper {
    OrderEntity toEntity(Order domain);
    Order toDomain(OrderEntity entity);
}

3. トランザクション境界

解決策

// ユースケース(ドメインサービス)にトランザクションを配置
@Service
@Transactional  // ← ここでトランザクション管理
public class OrderService implements CreateOrderUseCase {
    // ...
}

// アダプター層ではトランザクション不要
@Repository
public class JpaOrderRepository implements OrderRepository {
    // トランザクションなし
}

4. 複数のアダプター実装の切り替え

// application.yml で切り替え
@Configuration
public class RepositoryConfig {

    @Bean
    @ConditionalOnProperty(name = "storage.type", havingValue = "jpa")
    public OrderRepository jpaOrderRepository(SpringDataOrderRepository repo) {
        return new JpaOrderRepository(repo);
    }

    @Bean
    @ConditionalOnProperty(name = "storage.type", havingValue = "mongodb")
    public OrderRepository mongoOrderRepository(MongoTemplate template) {
        return new MongoOrderRepository(template);
    }
}

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

重要な原則

  1. ドメイン層は外部に依存しない

    • Spring、JPA、外部ライブラリに依存しない純粋なJava
  2. ポートはドメイン層に配置

    • インターフェースはビジネスロジック側が定義
  3. アダプターは差し替え可能

    • テスト時はモック、本番はJPA等
  4. ユースケースを明示

    • CreateOrderUseCaseのように意図が明確

適用シーン

推奨

  • ビジネスロジックが複雑
  • 長期運用が予想されるシステム
  • 外部依存(DB、API等)が頻繁に変わる可能性
  • テスタビリティ重視

過剰設計の可能性

  • シンプルなCRUDアプリ
  • プロトタイプ・PoC
  • 小規模な内部ツール

📚 付録

用語集

  • Port(ポート): ドメイン層が外部とやり取りするインターフェース
  • Adapter(アダプター): ポートの具体的な実装(技術的詳細)
  • Primary Port: アプリケーションを駆動する入力(API、CLI等)
  • Secondary Port: アプリケーションが利用する出力(DB、外部API等)
  • Driving Side: アプリケーションを使う側(UI、API呼び出し側)
  • Driven Side: アプリケーションが使う側(DB、外部サービス)

推奨参考資料

練習課題

課題1: シンプルなTODOアプリをヘキサゴナルアーキテクチャで実装してみましょう

  • プライマリポート: CreateTodoUseCase, GetTodoListUseCase
  • セカンダリポート: TodoRepository
  • 入力アダプター: REST API
  • 出力アダプター: InMemory実装 → JPA実装へ差し替え

課題2: 既存のレイヤードアーキテクチャをヘキサゴナルにリファクタリングしてみましょう

課題3: 同じポートに対して複数のアダプター(JPA/MongoDB/InMemory)を実装し、設定で切り替えてみましょう


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

  • 【アーキテクチャ】クリーンアーキテクチャ - ヘキサゴナルの発展形
  • 【アーキテクチャ】ドメイン駆動設計(DDD) - ドメインモデルの設計手法
  • 【アーキテクチャ】モジュラモノリス - モジュール内部でヘキサゴナルを適用
  • 【アーキテクチャ】CQRS(コマンドクエリ責任分離) - ヘキサゴナルと組み合わせ可能
  • 【プログラミング】【Java】Spring Framework - DIコンテナの活用

📝 最終更新: 2025-10-25

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