サービス モジュールの相互作用のコンテキストでは、次のような疑問が必然的に生じます。通信はどのようなルールで行われるのでしょうか。IT 製品では、「契約」はシステム間でどのようなデータが流れ、どのように送信されるかについての正式な理解を表します。これには、データ形式 (JSON、Protobuf など)、構造要素 (フィールド、データ型)、通信プロトコル (REST、gRPC、メッセージ キュー)、およびその他の仕様が含まれます。
契約により、オープン性 (誰もが受信および送信内容を把握)、予測可能性 (契約を更新してバージョンを維持できる)、信頼性 (適切に管理された変更を行った場合、システムが故障しない) が保証されます。
実際には、誰もがマイクロサービスや「契約」、API について話しているにもかかわらず、「API を構築する代わりに、データベースに共有テーブルを作成してみませんか?」というアプローチを採用する人をよく見かけます。
したがって、データ交換に共有テーブルを使用すると、効率的で、迅速な結果を得るために最適化されているように見えますが、長期的にはさまざまな技術的および組織的な課題が発生します。ただし、チームがデータ交換に共有テーブルを選択すると、実装中にさまざまな問題に直面する可能性があります。
サービスが REST/gRPC/GraphQL 経由で通信する場合、OpenAPI (Swagger)、protobuf スキーマ、または GraphQL スキーマという正式な定義があります。これらは、使用可能なリソース (エンドポイント)、想定されるフィールド、そのタイプ、および要求/応答の形式を詳細に定義します。「共有テーブル」が契約として機能する場合、正式な説明はありません。契約の正式な説明はありません。テーブル スキーマ (DDL) のみが使用可能ですが、それも十分に文書化されていません。テーブル構造の小さな変更 (列の追加または削除、データ型の変更など) は、このテーブルを読み書きする他のチームに影響を与える可能性があります。
API のバージョン管理は通常の方法です。バージョン 1、バージョン 2 などがあり、下位互換性を維持しながら、クライアントを徐々に新しいバージョンに移行できます。データベース テーブルの場合、特定の DB エンジンに密接に結合されており、移行を慎重に処理する必要がある DDL 操作 ( ALTER TABLE
など) のみがあります。
クエリの更新を必要とするスキーマの変更について消費者に警告を送信できる集中システムはありません。その結果、 「裏取引」が発生する可能性があります。誰かがチャットに「明日、列 X を Y に変更します」と投稿しても、全員が時間内に準備できる保証はありません。
API が明確に定義されている場合、その所有者は誰であるかが明らかです。つまり、API 発行元として機能するサービスです。複数のチームが同じデータベース テーブルを使用すると、構造や保存するフィールド、それらの解釈方法を誰が決定するかについて混乱が生じます。その結果、テーブルは「誰の所有物でもない」ものになり、変更のたびに「他のチームが古い列を使用しているかどうかを確認する必要がある」という課題が生じます。
多くのチームが DB にアクセスできる場合、誰がテーブルを読み書きできるかを追跡するのは困難です。意図していないデータであっても、権限のないサービスがデータにアクセスできる可能性があります。API を使用すると、このような問題の管理が簡単になります。アクセス権 (どのメソッドを誰が呼び出せるか) を制御し、認証と承認を使用し、誰が何を呼び出したかを監視できます。テーブルの場合は、はるかに複雑になります。
データに対する内部変更 (インデックスの再編成、テーブルのパーティション分割、DB の変更) は、グローバルな問題になります。テーブルがパブリック インターフェイスとして機能する場合、所有者はすべての外部のリーダーとライターを危険にさらすことなく内部変更を行うことはできません。
これは最も面倒な点です。次の日にスキーマが変更されることを他のチームにどうやって通知するか?
複数のチームが共有テーブルを使用して重要なデータを選択および更新すると、そのテーブルは簡単に「戦場」になります。その結果、ビジネス ロジックがさまざまなサービスに分散され、データの整合性を一元的に制御できなくなります。特定のフィールドが特定の方法で保存されている理由、誰が更新できるか、空白のままにすると何が起こるかを把握することは非常に困難になります。
たとえば、テーブルが壊れたとします。つまり、不正なデータがあったり、誰かが重要な行をロックしたとします。問題の原因を特定するには、多くの場合、DB アクセス権を持つすべてのチームに、どのクエリが問題を引き起こしたのかを問い合わせる必要があります。原因は明らかではないことがよくあります。つまり、あるチームのクエリがデータベースをロックし、別のチームのクエリが目に見えるエラーを生成している可能性があります。
共有データベースは単一障害点です。データベースがダウンすると、多くのサービスもダウンします。1 つのサービスのクエリが集中したためにデータベースのパフォーマンスに問題が発生すると、すべてのサービスに問題が発生します。明確な API とデータ所有権を備えたモデルでは、各チームがサービスの可用性とパフォーマンスを管理できるため、1 つのコンポーネントの障害が他のコンポーネントに波及することはありません。
よくある妥協案は、「メイン データベースに影響を与えずにクエリを実行できるように、読み取り専用のレプリカを提供します」というものです。最初はこれで負荷の問題は解決されるかもしれませんが、次のような問題があります。
最新の設計手法 (「API ファースト」や「コントラクト ファースト」など) は、正式なインターフェース定義から始まります。OpenAPI/Swagger、protobuf、または GraphQL スキーマが使用されます。これにより、人間とマシンの両方が、どのエンドポイントが利用可能か、どのフィールドが必須か、どのデータ型が使用されているかを把握できます。
マイクロサービス (またはモジュラー) アーキテクチャでは、各サービスがデータを完全に所有することが前提となっています。構造、ストレージ、ビジネス ロジックを定義し、その API へのすべての外部アクセス用の API を提供します。誰も「他人の」データベースに触れることはできません。触れることができるのは公式のエンドポイントまたはイベントだけです。これにより、変更が問題になるたびに作業が楽になり、誰が責任を負うのか常に明確になります。
GET /items
、 POST /items
などのエンドポイントを公開し、クライアントは明確に定義されたデータ スキーマ (DTO) を使用してリクエストを行います。
どのモデルであっても、インターフェースにバージョン管理を実装することは可能であり、また不可欠です。例:
基本的な原則は、データを所有するチームがデータの保存方法と管理方法を決定しますが、他のサービスに直接書き込みアクセス権を与えてはならないということです。他のユーザーは、外部データを編集するのではなく、API を経由する必要があります。これにより、責任の分散が明確になります。サービス A が故障した場合、それを修復するのはサービス A の責任であり、隣接するサービスではありません。
一見すると、すべてが 1 つのチームにあるのに、なぜ API で物事を複雑にするのでしょうか。実際には、単一の製品をモジュールに分割している場合でも、共有テーブルによって同じ問題が発生する可能性があります。
たとえば、Orders サービスは注文テーブルの所有者であり、Billing サービスはそのテーブルに直接アクセスしません。注文の詳細を取得したり、注文を支払済みとしてマークしたりするために、Orders サービスのエンドポイントを呼び出します。
より高いレベルでは、2 つ以上のチームが異なる領域を担当する場合でも、原則は同じです。たとえば、次のようになります。
チーム B がチーム A に属する「カタログ」テーブルを直接クエリすると、チーム A での内部スキーマ変更 (フィールドの追加、構造の変更など) がチーム B に影響を及ぼす可能性があります。
適切なアプローチは API を使用することです。チーム A はGET /catalog/items
、 GET /catalog/items/{id}
などのエンドポイントを提供し、チーム B はそれらのメソッドを使用します。チーム A が古いバージョンと新しいバージョンをサポートできる場合は、/v2 をリリースして、B に移行する時間を与えることができます。
正式な契約があれば、Swagger/OpenAPI、.proto ファイル、イベント ドキュメントなど、すべての変更が可視化されます。更新は事前に話し合い、適切にテストし、必要に応じて下位互換性戦略を使用してスケジュールすることができます。
1 つのサービスの変更が他のサービスに与える影響は少なくなります。チームは、新しいフィールドやエンドポイント、古いフィールドやエンドポイントを適切に管理していれば、他の誰かに「影響を与える」ことを心配する必要がなく、スムーズな移行が保証されます。
API ゲートウェイ、認証、承認 (JWT、OAuth) はサービスでは標準ですが、共有テーブルではほぼ不可能です。アクセスを微調整 (誰がどのメソッドを呼び出せるか)、ログの保存、使用統計の追跡、クォータの適用が簡単になります。これにより、システムの安全性と予測可能性が高まります。
データベース内の共有テーブルは、サービス間の合意ではなく実装の詳細であるため、契約とは見なされません。多くの問題 (複雑なバージョン管理、無秩序な変更、不明確な所有権、セキュリティ、パフォーマンスのリスク) があるため、このアプローチは長期的には実行不可能です。
正しいアプローチは、 Contract Firstです。これは、正式な設計を通じてインタラクションを定義し、各サービスがデータの所有者であり続けるという原則に従うことを意味します。これにより、技術的負債が軽減されるだけでなく、透明性が向上し、製品開発がスピードアップし、データベース スキーマの消火活動に追われることなく安全な変更が可能になります。
これは技術的な問題 (設計と統合の方法) と組織的な問題 (チームがどのようにコミュニケーションを取り、変更を管理するか) の両方です。データベース スキーマに関する無限の緊急事態に対処せずに製品を成長させたいのであれば、データベースへの直接アクセスではなく、契約の観点から考え始める必要があります。