ストーリー
通信プロトコルの選択
REST vs gRPC vs GraphQL
| 項目 | REST | gRPC | GraphQL |
|---|---|---|---|
| プロトコル | HTTP/1.1 | HTTP/2 | HTTP/1.1 |
| データ形式 | JSON | Protocol Buffers | JSON |
| 型安全性 | △(OpenAPI) | ◎(Proto定義) | ○(Schema) |
| パフォーマンス | 中 | 高 | 中 |
| ストリーミング | △(SSE) | ◎(双方向) | △(Subscription) |
| ブラウザ対応 | ◎ | △(gRPC-Web) | ◎ |
| 学習コスト | 低 | 中 | 中 |
| 主な用途 | 外部API | サービス間通信 | BFF/フロント向け |
使い分けの指針
外部公開API → REST(広い互換性)
サービス間同期通信 → gRPC(型安全・高速)
フロントエンド向け → GraphQL(柔軟なクエリ)
サービス間非同期 → イベント(Kafka/SQS)
API契約の設計原則
Consumer-Driven Contracts
消費者(呼び出し側)が必要な契約を定義し、提供者がそれを満たす:
// Consumer(注文サービス)が定義する契約
// "商品サービスにはこのレスポンスを期待する"
interface ProductContract {
id: string;
name: string;
price: number;
inStock: boolean;
// 注文サービスはこの4フィールドだけ必要
// 商品の画像URLやカテゴリは不要
}
// Pact によるコントラクトテスト
describe('Product Service Contract', () => {
it('returns product details', async () => {
// Consumer が期待するレスポンスを定義
await provider.addInteraction({
state: 'product ABC exists',
uponReceiving: 'a request for product ABC',
withRequest: {
method: 'GET',
path: '/products/ABC',
},
willRespondWith: {
status: 200,
body: {
id: like('ABC'),
name: like('商品名'),
price: like(1000),
inStock: like(true),
},
},
});
});
});
APIバージョニング戦略
| 戦略 | 方法 | メリット | デメリット |
|---|---|---|---|
| URLパス | /v1/products | 明確、キャッシュしやすい | URL変更が必要 |
| ヘッダー | Accept: application/vnd.api.v2+json | URLが変わらない | 発見しにくい |
| クエリパラメータ | ?version=2 | シンプル | キャッシュキー複雑 |
後方互換性の維持
// ✅ 後方互換: フィールド追加(既存クライアントは無視可能)
// v1: { id, name, price }
// v2: { id, name, price, description, category }
// ❌ 破壊的変更: フィールド名変更
// v1: { product_name }
// v2: { name } ← 既存クライアントが壊れる
// ✅ 推奨: Tolerant Reader パターン
class ProductClient {
parseResponse(data: unknown): Product {
// 未知のフィールドは無視する
const parsed = productSchema.safeParse(data);
if (!parsed.success) {
// フォールバック処理
return this.parseV1Response(data);
}
return parsed.data;
}
}
gRPC によるサービス間通信
// product.proto
syntax = "proto3";
service ProductService {
rpc GetProduct(GetProductRequest) returns (Product);
rpc ListProducts(ListProductsRequest) returns (stream Product);
rpc UpdateStock(UpdateStockRequest) returns (UpdateStockResponse);
}
message Product {
string id = 1;
string name = 2;
int64 price_cents = 3;
bool in_stock = 4;
// 新フィールドは番号を追加するだけ(後方互換)
string description = 5;
}
message GetProductRequest {
string product_id = 1;
}
// gRPC クライアント実装
class ProductGrpcClient {
private client: ProductServiceClient;
constructor(address: string) {
this.client = new ProductServiceClient(
address,
grpc.credentials.createInsecure()
);
}
async getProduct(productId: string): Promise<Product> {
return new Promise((resolve, reject) => {
this.client.getProduct(
{ productId },
(error, response) => {
if (error) reject(error);
else resolve(response);
}
);
});
}
}
API ゲートウェイパターン
graph TD
Client["クライアント"] --> GW["API Gateway\n認証/認可\nレート制限\nルーティング\nレスポンス集約\nプロトコル変換"]
GW --> OrderSvc["注文 svc"]
GW --> ProductSvc["商品 svc"]
GW --> CustomerSvc["顧客 svc"]
style Client fill:#dbeafe,stroke:#2563eb,color:#1e40af
style GW fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#5b21b6
style OrderSvc fill:#d1fae5,stroke:#059669,color:#065f46
style ProductSvc fill:#d1fae5,stroke:#059669,color:#065f46
style CustomerSvc fill:#d1fae5,stroke:#059669,color:#065f46
BFF(Backend for Frontend)パターン:
Web Client → Web BFF → 各サービス
Mobile App → Mobile BFF → 各サービス
Admin UI → Admin BFF → 各サービス
まとめ
| ポイント | 内容 |
|---|---|
| プロトコル選択 | 用途に応じてREST/gRPC/GraphQLを使い分け |
| Consumer-Driven | 消費者が契約を定義し、コントラクトテスト |
| バージョニング | 後方互換性を維持しながら進化 |
| API Gateway | 横断的関心事を集約し、BFFで最適化 |
チェックリスト
- REST/gRPC/GraphQL の使い分けを判断できる
- Consumer-Driven Contract の概念を理解した
- API バージョニング戦略を選択できる
- API Gateway/BFF パターンを説明できる
次のステップへ
次は演習でサービス境界設計に挑戦します。
推定読了時間: 40分