内部サービスと外部コンシューマー向けAPIのテーラリング
Olivia Novak
Dev Intern · Leapcell

はじめに
バックエンド開発の複雑な世界では、効果的なAPI設計が最も重要です。しかし、「万能」アプローチは、さまざまなAPIコンシューマーの多様な要件に対応する際には、しばしば不十分です。具体的には、gRPCやRPCのような高性能プロトコルを介して通信する内部サービスと、RESTやGraphQLのようなより標準化されたインターフェースを介して対話する外部クライアントのニーズは、著しく異なります。この相違により、各ユースケースに最適化された、異なるAPI設計戦略が必要となります。これらの違いを理解し、意識的に適切なアプローチを選択することで、より高性能で、保守しやすく、スケーラブルなシステムを構築でき、最終的には全体的な開発者エクスペリエンスを向上させ、製品提供を加速させることができます。この記事では、これらの異なる戦略を掘り下げ、意図されたオーディエンスに真に応えるAPIを設計するための包括的なガイドを提供します。
コアコンセプトの理解
設計戦略に着手する前に、この議論の基盤となるコア用語を明確にしましょう。
- gRPC (gRPC Remote Procedure Call): Googleが開発した、高性能なオープンソースのユニバーサルRPCフレームワークです。インターフェース定義言語(IDL)およびメッセージ交換フォーマットとしてProtocol Buffers (protobuf) を使用し、効率的なデータシリアライゼーションとデシリアライゼーションを可能にします。gRPCはさまざまなプログラミング言語をサポートし、HTTP/2 overで動作し、双方向ストリーミング、フロー制御、ヘッダー圧縮などの機能を提供します。
- RPC (Remote Procedure Call): プログラムが、リモートインタラクションの詳細を明示的にコーディングすることなく、別の名前空間(通常は共有ネットワーク上の別のコンピュータ)でプロシージャ(サブルーチンまたは関数)を実行できるようにする、基本的な通信パラダイムです。
- REST (Representational State Transfer): 分散ハイパーメディアシステムを設計するためのアーキテクチャスタイルです。REST APIは、標準的なHTTPメソッド(GET、POST、PUT、DELETE)と、リソースなどの概念を活用し、データ交換にはJSONまたはXMLを使用することがよくあります。ステートレスであり、シンプルさ、スケーラビリティ、および広範なクライアント互換性を重視します。
- GraphQL: APIのためのクエリ言語であり、既存のデータでこれらのクエリを満たすための実行環境です。Facebookによって開発されたGraphQLは、クライアントが必要なデータだけを、それ以上でもそれ以下でもなく要求することを可能にします。通常、単一のエンドポイントを使用し、クライアントがレスポンスの構造を定義できるようにすることで、過剰取得(over-fetching)や過少取得(under-fetching)を削減します。
内部サービス(gRPC/RPC)向けAPIの設計
内部サービスは、パフォーマンス、効率性、および型安全性を優先することがよくあります。これらは管理されたエコシステム内で動作するため、焦点は広範な互換性から最適化されたサービス間通信に移行します。
原則
- 厳密に定義された契約: Protocol Buffers(gRPCの場合)やAvro(一部のRPC実装の場合)のようなIDLを活用して、サービスインターフェースとメッセージ構造を定義します。これにより、サービス全体で強力な型安全性と一貫性が保証されます。
- パフォーマンス最適化: 効率的なデータシリアライゼーション(バイナリフォーマット)を重視し、オーバーヘッドを最小限に抑えます。gRPCのHTTP/2基盤とストリーミング機能は、これに優れています。
- ドメイン駆動設計 (DDD): 内部サービス向けのAPIは、内部ドメインモデルをより直接的に反映することがよくあります。これにより、より詳細な、操作中心のAPIにつながる可能性があります。
- エラーハンドリング: 詳細でプログラム可能なエラーコードとメッセージは、一般的なHTTPステータスコードよりも有用です。
実装と例(Goを使用したgRPC)
シンプルな内部ユーザー管理サービスを想像してみましょう。
まず、.proto ファイルでサービスとメッセージを定義します。
// api/user_management_service.proto syntax = "proto3"; package usermanagement; option go_package = "./usermanagement"; service UserManagementService { rpc GetUser(GetUserRequest) returns (GetUserResponse); rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); rpc UpdateUser(UpdateUserRequest) returns (UpdateResponse); } message GetUserRequest { string user_id = 1; } message GetUserResponse { User user = 1; } message CreateUserRequest { string username = 1; string email = 2; } message CreateUserResponse { string user_id = 1; } message UpdateUserRequest { string user_id = 1; string username = 2; string email = 3; } message UpdateResponse { bool success = 1; string message = 2; } message User { string id = 1; string username = 2; string email = 3; string created_at = 4; }
このprotoファイルは正確な契約を定義します。protoc のようなツールは、さまざまな言語でコードを生成します。
Goサーバー実装のスニペットを以下に示します。
// internal/server/user_server.go package server import ( "context" "fmt" // エラー例用 pb "your-project/pkg/usermanagement" // 生成されたprotoパッケージ ) type UserManagementServer struct { pb.UnimplementedUserManagementServiceServer // 設計によっては、リポジトリやサービスレイヤーがここにある場合があります // userRepo repository.UserRepository } // GetUser はIDによるユーザー取得リクエストを処理します func (s *UserManagementServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) { fmt.Printf("Received GetUser request for user_id: %s\n", req.GetUserId()) // 実際のアプリケーションでは、データベースから取得します if req.GetUserId() == "123" { return &pb.GetUserResponse{ User: &pb.User{ Id: "123", Username: "johndoe", Email: "john@example.com", CreatedAt: "2023-01-01T10:00:00Z", }, }, nil } return nil, fmt.Errorf("user not found: %s", req.GetUserId()) } // CreateUser は新規ユーザー作成リクエストを処理します func (s *UserManagementServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) { fmt.Printf("Received CreateUser request for username: %s, email: %s\n", req.GetUsername(), req.GetEmail()) // DBへのユーザー作成、ID生成のロジック newUserID := "456" // モックID return &pb.CreateUserResponse{UserId: newUserID}, nil } // UpdateUser は既存ユーザー更新リクエストを処理します func (s *UserManagementServer) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateResponse, error) { fmt.Printf("Received UpdateUser request for user_id: %s, new username: %s\n", req.GetUserId(), req.GetUsername()) // DBへのユーザー更新ロジック return &pb.UpdateResponse{Success: true, Message: "User updated successfully"}, nil }
そして、この内部サービスを呼び出すクライアントです。
// internal/client/user_client.go package client import ( "context" "log" pb "your-project/pkg/usermanagement" // 生成されたprotoパッケージ "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func CallUserManagementService() { conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewUserManagementServiceClient(conn) // ユーザーを取得 res, err := c.GetUser(context.Background(), &pb.GetUserRequest{UserId: "123"}) if err != nil { log.Printf("could not get user: %v", err) } else { log.Printf("User: %v", res.GetUser()) } // ユーザーを作成 createRes, err := c.CreateUser(context.Background(), &pb.CreateUserRequest{Username: "alice", Email: "alice@example.com"}) if err != nil { log.Printf("could not create user: %v", err) } else { log.Printf("Created User ID: %s", createRes.GetUserId()) } // ユーザーを更新 updateRes, err := c.UpdateUser(context.Background(), &pb.UpdateUserRequest{UserId: "456", Username: "alice_updated"}) if err != nil { log.Printf("could not update user: %v", err) } else { log.Printf("Update successful: %v", updateRes.GetSuccess()) } }
この例は、gRPCの強力な型付けと直接的なメソッド呼び出しの特性を示しており、内部サービス間通信に最適です。
外部クライアント(REST/GraphQL)向けAPIの設計
Webブラウザ、モバイルアプリ、サードパーティの統合など、外部クライアントは、使いやすさ、検出可能性、広範な言語サポート、および柔軟性といった、異なる品質を要求します。
原則
- リソース指向(REST): 特定の操作ではなく、ビジネスリソースを中心にAPIを構造化します。これらのリソースに対するアクションを実行するために、標準HTTPメソッドを使用します。
- 柔軟なデータ取得(GraphQL): クライアントが、過剰取得や過少取得を回避するために、必要なデータの定義を可能にします。
- 自己記述的: ドキュメント、RESTの場合はOpenAPI/Swagger、GraphQLの場合はイントロスペクションスキーマを介して、明確なドキュメントを提供します。
- エラーハンドリング: 問題を伝達するために、標準HTTPステータスコード(RESTの場合)または明確に定義されたエラーオブジェクト構造(GraphQLの場合)を使用します。
- バージョン管理: 既存のクライアントへの破壊的変更を防ぐために、APIの進化を計画します。
- セキュリティ: 強力な認証(OAuth2、JWT)および認可メカニズムを実装します。
実装と例(Goを使用したREST、GraphQLの概念)
ユーザー情報のための公開REST APIを公開してみましょう。このREST APIは、内部gRPCサービスを利用する可能性があります。
REST API(Goのnet/httpを使用)
// external/api/rest_user_handler.go package api import ( "encoding/json" "fmt" "log" "net/http" "github.com/gorilla/mux" // GoでのREST APIの人気のあるルーター pb "your-project/pkg/usermanagement" // 生成されたprotoパッケージ "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "context" ) // RESTハンドラは通常、サービスレイヤーと対話し、それは内部gRPCサービスを呼び出します。 type UserRESTHandler struct { // 内部gRPCサービスのクライアント grpcClient pb.UserManagementServiceClient } func NewUserRESTHandler() *UserRESTHandler { conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("REST handler could not connect to gRPC server: %v", err) } // 注意:本番システムでは、singletonや依存性注入などでこの接続を慎重に管理してください。 return &UserRESTHandler{ grpcClient: pb.NewUserManagementServiceClient(conn), } } // GetUser は GET /users/{id} を処理します func (h *UserRESTHandler) GetUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) userID := vars["id"] grpcReq := &pb.GetUserRequest{UserId: userID} grpcRes, err := h.grpcClient.GetUser(context.Background(), grpcReq) if err != nil { http.Error(w, fmt.Sprintf("Failed to fetch user from internal service: %v", err), http.StatusInternalServerError) return } if grpcRes.GetUser() == nil { // ユーザーが実際に返されたかを確認、gRPCエラーハンドリングに基づく http.Error(w, "User not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ // protoユーザーをシンプルなJSONオブジェクトにマッピング "id": grpcRes.GetUser().GetId(), "username": grpcRes.GetUser().GetUsername(), "email": grpcRes.GetUser().GetEmail(), "createdAt": grpcRes.GetUser().GetCreatedAt(), }) } // CreateUser は POST /users を処理します func (h *UserRESTHandler) CreateUser(w http.ResponseWriter, r *http.Request) { var requestBody struct { Username string `json:"username"` Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } grpcReq := &pb.CreateUserRequest{ Username: requestBody.Username, Email: requestBody.Email, } grpcRes, err := h.grpcClient.CreateUser(context.Background(), grpcReq) if err != nil { http.Error(w, fmt.Sprintf("Failed to create user in internal service: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"id": grpcRes.GetUserId()}) } // ルーターの設定 // func main() { // router := mux.NewRouter() // handler := NewUserRESTHandler() // router.HandleFunc("/users/{id}", handler.GetUser).Methods("GET") // router.HandleFunc("/users", handler.CreateUser).Methods("POST") // log.Fatal(http.ListenAndServe(":8080", router)) // }
このREST APIは、リソース中心のビュー(例:/users/{id})を提示し、標準HTTP動詞を使用します。APIゲートウェイとして機能し、外部RESTリクエストを内部gRPC呼び出しに変換します。
GraphQL(概念)
GraphQLでは、クライアントが特定のユーザーフィールドをクエリできるようにするスキーマを定義します。
# schema.graphql type User { id: ID! username: String! email: String createdAt: String } type Query { user(id: ID!): User } type Mutation { createUser(username: String!, email: String): User }
GraphQLリゾルバ(ここでもGo、Node.jsなどで実装可能)は、これらのGraphQLクエリとミューテーションを、RESTハンドラと同様に、内部gRPCサービスへの呼び出しにマッピングします。
GraphQLリゾルバースニペット(概念Go):
// external/api/graphql_resolvers.go package api import ( "context" pb "your-project/pkg/usermanagement" // 生成されたprotoパッケージ ) type Resolver struct { grpcClient pb.UserManagementServiceClient } func (r *Resolver) Query_user(ctx context.Context, args struct{ ID string }) (*User, error) { grpcReq := &pb.GetUserRequest{UserId: args.ID} grpcRes, err := r.grpcClient.GetUser(ctx, grpcReq) if err != nil { // gRPCエラーを処理し、GraphQLエラーにマッピングします return nil, err } if grpcRes.GetUser() == nil { return nil, nil // GraphQLクライアントは、見つからない場合はnullを期待します } return &User{ ID: grpcRes.GetUser().GetId(), Username: grpcRes.GetUser().GetUsername(), Email: grpcRes.GetUser().GetEmail(), CreatedAt: grpcRes.GetUser().GetCreatedAt(), }, nil } // ミューテーションについても同様
これは、RESTまたはGraphQLのいずれであっても、外部APIが内部通信の詳細を抽象化し、クライアントフレンドリーなインターフェースを提供する方法を示しています。
アプリケーションシナリオ
-
内部サービス(gRPC/RPC):
- 大規模分散システム内でのマイクロサービス通信。
- シリアライゼーションとデシリアライゼーションのオーバーヘッドを最小限に抑える必要がある、高スループットのデータパイプライン。
- 効率的で型安全なバックエンドコンポーネント間通信の構築。
- サービス間でのデータストリーミング(例:リアルタイム分析)。
-
外部クライアント(REST/GraphQL):
- Webおよびモバイルアプリケーション向けの公開API。
- 広範な互換性が重要であるサードパーティ統合ポイント。
- データ要件を正確に定義できる柔軟なフロントエンドの開発。
- 標準的なエンタープライズアプリケーション統合(REST)。
結論
APIを効果的に設計するには、内部サービス間通信と外部クライアントインタラクションを区別する思慮深いアプローチが必要です。内部サービスの場合、gRPC/RPCは、バイナリプロトコルと強力な契約を活用することで、比類のないパフォーマンス、型安全性、および効率性を提供します。外部コンシューマーの場合、RESTは広範な採用とリソース指向のシンプルさを提供し、GraphQLはクライアント駆動のデータ取得の柔軟性を提供します。これらの異なる戦略を意識的に適用することで、開発者は、各タイプのコンシューマーのニーズに正確に応える、堅牢で最適化された保守可能なバックエンドシステムを構築でき、より効率的な開発と全体的なシステムパフォーマンスの向上につながります。本質は、適切なオーディエンスに適切なインターフェースを提供することにあります。

