RESTful API データモデリング

外部サービス連携時のAPIデータモデリング:外部モデルと内部モデルの分離戦略

Tags: API設計, データモデリング, 外部API連携, ACL, アンチ・カリープ・レイヤー, ドメイン駆動設計

はじめに:外部API連携とデータモデリングの課題

現代のシステム開発において、外部のサービスやAPIと連携することは一般的です。決済代行サービス、ソーシャルログインプロバイダー、地図情報サービスなど、様々な外部APIを利用することで、システムはよりリッチな機能を提供できます。

しかし、外部APIとの連携は、データモデリングの観点からいくつかの特有の課題をもたらします。外部APIが提供するデータの構造は、必ずしも自社のシステム内部で定義しているドメインモデルと一致しないことがほとんどです。また、外部APIは提供側の都合で変更される可能性があり、その変更が自社システムに影響を及ぼすことも考慮しなければなりません。

このような状況で、単に外部APIから取得したデータをそのまま利用したり、その構造に合わせて内部モデルを定義したりすると、以下のような問題が発生しやすくなります。

これらの課題に対処し、変更に強く保守性の高いシステム、そしてそれを支える堅牢なAPIを設計するためには、外部APIのデータ構造と自社の内部ドメインモデルを適切に「分離」する考え方が非常に重要になります。本記事では、この分離戦略に焦点を当て、具体的なデータモデリングのアプローチについて解説します。

外部API連携におけるデータモデリングの課題詳細

外部API連携において、具体的にどのようなデータモデリングの課題が生じやすいかを見てみましょう。

  1. データ構造の不一致:

    • 外部APIのエンティティ名、プロパティ名、データ型、ネスト構造などが、自社のドメインモデルと異なる。
    • 外部APIのレスポンスには、自社システムでは不要な情報が含まれていることが多い。
    • 外部APIは特定の用途に特化しており、汎用的なドメインモデルとは視点が異なる。

    (例:外部APIがユーザー情報を返す際に、user_id, user_name, email_address といったプロパティ名を使用しているが、自社システムでは id, name, email といった名前を使用している。)

  2. データの粒度や表現方法の違い:

    • 外部APIが返すデータは、自社のシステムで必要とする粒度よりも細かい、あるいは粗い場合がある。
    • 状態の表現方法(例:数値コード、文字列、Enum)が異なる。
    • 日付や時刻のフォーマット、タイムゾーンの扱いが異なる。

    (例:外部APIが注文ステータスを数値コード 10: Pending, 20: Processing, 30: Completed で表現するが、自社システムではEnum OrderStatus.PENDING, OrderStatus.PROCESSING, OrderStatus.COMPLETED で管理している。)

  3. 変更への対応:

    • 外部APIがバージョンアップによりレスポンス構造を変更する可能性がある(プロパティの追加・変更・削除、構造の変更など)。
    • 予期しない変更やドキュメント化されていない仕様変更が発生する可能性もある。
  4. エラーハンドリングの不一致:

    • 外部APIのエラーレスポンスの構造やエラーコード体系が、自社システムのエラーハンドリング基準と異なる。

これらの課題は、システム全体、特に外部連携部分のコードの複雑性を増大させ、将来的な保守や機能追加の大きな妨げとなり得ます。

なぜ外部モデルと内部モデルの分離が必要なのか

外部APIのデータ構造から自社の内部ドメインモデルを分離する主な目的は、自社システムの独立性変更容易性を高めることにあります。

外部モデルと内部モデルの分離戦略:アンチ・カリープ・レイヤー(ACL)

外部APIのデータ構造と自社の内部ドメインモデルを分離するための代表的な設計パターンとして、アンチ・カリープ・レイヤー (Anti-Corruption Layer - ACL) があります。これは、ドメイン駆動設計(DDD)における戦略的設計のパターンの一つですが、DDDを本格的に採用していないシステムでも、外部システムとの連携において非常に有効な考え方です。

ACLの基本的な考え方は以下の通りです。

これにより、内部ドメインモデルは外部APIの「腐敗」(異質な構造や変更)から守られ、健全な状態を保つことができます。

ACLによるデータ変換(マッピング)の実装

ACLの実装は、外部APIのレスポンスデータを内部モデルのオブジェクトにマッピングする処理が中心となります。このマッピング処理は、ACLとして独立したモジュールやクラスとして実装することが推奨されます。

具体的なデータ変換の例を見てみましょう。

仮に、以下のような外部APIのユーザー取得レスポンスがあるとします。

{
  "id": "user123",
  "displayName": "Alice Smith",
  "email": "alice.smith@example.com",
  "registrationTimestamp": 1678886400,
  "status": "active",
  "metadata": {
    "lastLogin": 1678972800,
    "loginCount": 5
  }
}

一方、自社の内部ドメインモデルとしてのユーザーオブジェクトは以下のようになっているとします。

// Javaの例
public class User {
    private String userId;
    private String name;
    private String email;
    private ZonedDateTime registeredAt;
    private UserStatus status; // Enum型

    // コンストラクタ、getterなどを省略
}

public enum UserStatus {
    ACTIVE, INACTIVE, SUSPENDED;
}

この場合、ACL内では外部APIレスポンス(JSONまたはそれをパースしたオブジェクト)を、内部の User オブジェクトに変換するロジックを実装します。

// ACL内のマッピング処理(イメージ)
public class ExternalUserMapper {
    public User mapToInternalUser(ExternalUserApiResponse externalUser) {
        User user = new User();
        user.setUserId(externalUser.getId());
        user.setName(externalUser.getDisplayName());
        user.setEmail(externalUser.getEmail());
        // Unix Timestamp (秒) を ZonedDateTime に変換
        user.setRegisteredAt(ZonedDateTime.ofInstant(Instant.ofEpochSecond(externalUser.getRegistrationTimestamp()), ZoneOffset.UTC));
        // 文字列ステータスを Enum に変換
        user.setStatus(mapStatus(externalUser.getStatus()));
        // metadata など、内部モデルに不要なデータはマッピングしない

        return user;
    }

    private UserStatus mapStatus(String externalStatus) {
        switch (externalStatus) {
            case "active": return UserStatus.ACTIVE;
            case "inactive": return UserStatus.INACTIVE;
            case "suspended": return UserStatus.SUSPENDED;
            default: throw new IllegalArgumentException("Unknown status: " + externalStatus); // 未知の値への対応
        }
    }
}

// 外部APIレスポンスを表現するクラス(ACL内で使用)
public class ExternalUserApiResponse {
    private String id;
    private String displayName;
    private String email;
    private long registrationTimestamp;
    private String status;
    private Map<String, Object> metadata; // JSONのmetadataに対応
    // getterなどを省略
}

このマッピング処理では、プロパティ名の変換だけでなく、データ型の変換(Timestamp → ZonedDateTime)、表現形式の変換(文字列ステータス → Enum)、そして不要なデータの選別(metadataをマッピングしない)が行われています。

自社APIレスポンスの設計におけるACLの活用

ACLで外部データを内部モデルに変換した後、それをそのまま自社APIのレスポンスとして公開するのではなく、自社APIの用途に合わせたデータ構造に再編成することが重要です。

例えば、上記の User 内部モデルを持つシステムが、クライアント向けにユーザー情報を取得するAPI /users/{userId} を提供するとします。このAPIのレスポンス構造は、内部モデルに直接対応させつつも、クライアントにとって最も利用しやすい形に設計すべきです。

// 自社APIのユーザー情報レスポンス例
{
  "userId": "user123",
  "fullName": "Alice Smith",
  "emailAddress": "alice.smith@example.com",
  "registeredAt": "2023-03-15T09:00:00Z",
  "userStatus": "ACTIVE"
}

この例では、自社APIのレスポンスは内部モデルの構造に近いですが、プロパティ名(fullName, emailAddress, userStatus)はクライアントの使いやすさを考慮して命名されているかもしれません。また、日付形式もISO 8601形式になっています。

ACLで外部 → 内部への変換を行い、さらにAPI層で内部 → APIレスポンスへの変換を行うという二段階の変換が考えられます。

これにより、外部APIの変更はACL内に閉じ込められ、自社APIの構造は内部ドメインモデルとクライアントの要求に基づいて安定的に設計できます。

外部APIの変更への対応とACL

ACLパターンは、外部APIの変更に強く対応するための強力な武器となります。

例えば、外部APIが /v1/users から /v2/users に変わり、レスポンス構造が大きく変わった場合、以下のように対応できます。

これにより、外部APIのバージョンアップに対応する作業が、システム全体ではなくACLに関連する部分に集中します。

考慮事項とアンチパターン

ACLパターンを採用する際の考慮事項と、避けるべきアンチパターンについても触れておきます。

考慮事項

アンチパターン

まとめ:変更に強いAPI設計のために分離戦略を

外部API連携におけるデータモデリングは、単にデータの形を合わせるだけでなく、外部の都合による変更から自社システムを守り、内部ドメインモデルの健全性を保つための戦略的な取り組みです。

アンチ・カリープ・レイヤー(ACL)のような分離戦略を採用することで、外部APIのデータ構造と自社の内部ドメインモデルを明確に切り離すことができます。これにより、外部APIの仕様変更があった場合でも、その影響をACL内部に限定し、システム全体、特に自社が公開するAPIの構造を安定させることができます。

多少の初期コストはかかるかもしれませんが、長期的なシステム保守性、変更容易性、そして内部ドメインモデルの独立性を考えると、この分離戦略は非常に有効なアプローチです。外部API連携を設計する際には、ぜひACLの考え方を取り入れ、変更に強いAPI、そしてシステム全体を構築することを目指してください。