· 14 分で読める · 7,004 文字
GraphQL vs REST API:実務で必要なパフォーマンス比較と選択基準
GraphQLとREST APIは異なる設計思想に基づいており、パフォーマンス特性も大きく異なります。本記事では、実務で直面する具体的なシナリオに基づいて両者のパフォーマンスを比較し、プロジェクトに最適な選択をするための判断基準を提示します。
GraphQLとREST APIの基本的な違い
まず、両者のアーキテクチャの違いを理解することが重要です。REST APIはリソース指向設計で、エンドポイント(`/users`、`/posts`など)ごとに固定のデータ構造を返します。一方、GraphQLはクエリ言語で、クライアントが必要なフィールドを明示的に指定し、その部分だけを取得できる設計です。
sequenceDiagram
participant Client
participant Server
participant DB
rect rgb(200, 220, 255)
Note over Client,DB: REST API: 複数エンドポイントの呼び出し
Client->>Server: GET /api/users/1
Server->>DB: SELECT * FROM users WHERE id=1
DB-->>Server: User data
Server-->>Client: User object + Posts array + Comments array
end
rect rgb(220, 255, 200)
Note over Client,DB: GraphQL: 単一エンドポイント、クエリで指定
Client->>Server: query { user(id:1) { name posts { title } } }
Server->>DB: SELECT name, posts.title FROM users JOIN posts
DB-->>Server: Filtered data
Server-->>Client: { name, posts: [{title}] }
end
ネットワークパフォーマンスの実測比較
オーバーフェッチング問題
REST APIの大きな課題がオーバーフェッチング(over-fetching)です。例えば、ユーザーの名前とメールアドレスだけが必要な場合でも、以下のようにユーザーの全情報が返されます:
// REST API: GET /api/users/1
{
"id": 1,
"name": "田中太郎",
"email": "tanaka@example.com",
"avatar": "https://...",
"bio": "長い自己紹介文...",
"createdAt": "2024-01-01",
"updatedAt": "2024-01-15",
"preferences": { ... },
"metadata": { ... }
}
実務では、このような不要なデータ転送が積み重なると、特にモバイルネットワークで顕著な遅延につながります。GraphQLなら必要なフィールドだけを指定できます:
# GraphQL Query
query {
user(id: 1) {
name
email
}
}
# Response
{
"data": {
"user": {
"name": "田中太郎",
"email": "tanaka@example.com"
}
}
}
モバイルデバイスでの実測では、GraphQLはペイロードサイズを平均40~60%削減できることが報告されています。
アンダーフェッチング問題
一方、REST APIではアンダーフェッチング(under-fetching)も発生します。これは必要なデータを得るために複数のAPIコールが必要になる現象です:
// REST API: 複数リクエストが必要
async function getUserWithPostsAndComments(userId) {
// 1回目のリクエスト
const user = await fetch(`/api/users/${userId}`);
// 2回目のリクエスト
const posts = await fetch(`/api/users/${userId}/posts`);
// 3回目のリクエスト
const comments = await fetch(`/api/users/${userId}/comments`);
return { ...user, posts, comments };
}
// 合計: 3リクエスト、往復時間が3倍
これに対してGraphQLなら1回のリクエストで済みます:
# GraphQL: 1リクエストですべて取得
query {
user(id: 1) {
name
email
posts {
title
content
comments {
text
author
}
}
}
}
実務経験上、複雑なデータ関連を扱うモバイルアプリでは、REST APIの複数リクエストによる往復遅延が顕著です。GraphQLに移行したプロジェクトでは、平均的に60~80%のレイテンシ削減を確認しました。
キャッシング戦略の実装難度
REST APIのキャッシング戦略
REST APIはHTTPの標準的なキャッシング機構(ETag、Cache-Control ヘッダ)を活用できるため、ブラウザキャッシュやCDNで容易にキャッシュできます:
// REST API: 標準HTTPキャッシュが機能
fetch('/api/users/1', {
headers: {
'Cache-Control': 'max-age=3600' // 1時間キャッシュ
}
})
.then(response => {
// ブラウザが自動的にキャッシュを活用
console.log(response);
});
一方、GraphQLはPOSTリクエストを使用することが多く、HTTPキャッシングが効きにくいという課題があります。
GraphQLのキャッシング実装
GraphQLでキャッシングを実装するには、アプリケーション層でのキャッシュ戦略が必要です:
// Apollo Clientを使用したGraphQLキャッシング戦略
import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id'], // ユーザーをIDでキャッシュキーとする
fields: {
posts: {
// postsフィールドは5分間キャッシュ
merge(existing, incoming) {
return incoming;
}
}
}
}
}
}),
// ... その他の設定
});
GraphQLでキャッシュを効果的に機能させるには、実装が複雑になりやすい点に注意が必要です。
サーバーサイドの負荷と複雑性の比較
REST APIのサーバー実装
REST APIはシンプルなエンドポイント設計で、各エンドポイントが明確に定義されます:
# FastAPI (Python) での REST API実装
from fastapi import FastAPI
from typing import List
app = FastAPI()
@app.get("/api/users/{user_id}")
async def get_user(user_id: int):
# userテーブルから直接取得
return {"id": user_id, "name": "田中太郎", "email": "tanaka@example.com"}
@app.get("/api/users/{user_id}/posts")
async def get_user_posts(user_id: int):
# postsテーブルから該当データを取得
return [{"id": 1, "title": "記事1"}, {"id": 2, "title": "記事2"}]
GraphQLのサーバー実装
GraphQLはリゾルバー関数で柔軟に対応する必要があり、複雑なクエリに対してNプラス1問題が発生しやすいです:
# Strawberry GraphQL (Python) での実装例
import strawberry
from typing import List
@strawberry.type
class User:
id: int
name: str
email: str
@strawberry.field
async def posts(self) -> List['Post']:
# ここで各ユーザーの投稿を取得
# N個のユーザーに対してN回のクエリが実行される(N+1問題)
return await fetch_posts(self.id)
@strawberry.type
class Query:
@strawberry.field
async def user(self, id: int) -> User:
return await fetch_user(id)
GraphQLではN+1問題を回避するため、DataLoaderの導入が推奨されます:
# DataLoaderでN+1問題を解決
from strawberry.dataloaders import DataLoader
async def load_posts(user_ids: List[int]) -> List[List['Post']]:
# 一度に複数ユーザーの投稿をバッチ取得
# SELECT * FROM posts WHERE user_id IN (user_ids)
posts_by_user = {}
results = await fetch_posts_batch(user_ids)
return [posts_by_user.get(uid, []) for uid in user_ids]
@strawberry.type
class User:
id: int
name: str
@strawberry.field
async def posts(self, info) -> List['Post']:
# DataLoaderでバッチ処理
loader = info.context.get_post_loader()
return await loader.load(self.id)
flowchart TD
A["GraphQL クエリ受信"] --> B["N個のユーザーを取得"]
B --> C{"DataLoader
導入済み?"}
C -->|NO| D["N個のポストを
個別クエリで取得"]
D --> E["⚠️ N+1問題
パフォーマンス低下"]
C -->|YES| F["ユーザーIDを
バッチ化"]
F --> G["1回のクエリで
全ポストを取得"]
G --> H["✅ 効率的
パフォーマンス"]
実務シナリオ別の選択ガイド
REST APIを選ぶべき場面
- シンプルなCRUD操作:ブログの記事一覧、ユーザー管理など、データ構造が単純な場合
- HTTPキャッシングの活用が重要:CDNでのキャッシュが必須な、大規模なコンテンツ配信
- チーム経験が浅い:REST APIの方がシンプルで理解しやすく、開発スピードが速い
- レガシーシステムとの統合:既存のREST API資産が多い場合の保守性
- 監視・デバッグの単純さが優先:REST APIはリクエスト・レスポンスの内容が直感的
GraphQLを選ぶべき場面
- 複雑なデータ関連:複数のテーブルを関連させる必要がある場合(SNS、ECサイトなど)
- モバイルアプリ開発:ネットワーク効率が重要で、必要最小限のデータ取得が必須
- 複数のクライアント対応:web、mobile、デスクトップアプリで異なるデータ要件がある場合
- リアルタイム機能が必要:GraphQL Subscriptionでサーバープッシュが容易
- スケーラビリティ重視:ペイロード削減と複数リクエスト排除により、サーバー負荷を低減
パフォーマンス最適化の実装テクニック
GraphQLのパフォーマンス最適化
GraphQLでハイパフォーマンスを実現するには、複数の最適化手法の組み合わせが必要です:
// Apollo Serverでの最適化設定例
import { ApolloServer } from '@apollo/server';
import { DataSourceConfig } from '@apollo/datasource-rest';
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: {
// クエリの複雑さを制限
async requestDidParse(context) {
const complexity = getQueryComplexity(context.document);
if (complexity > 1000) {
throw new Error('Query too complex');
}
},
// 遅いクエリをログ
async willSendResponse(context) {
const duration = context.endHrTime - context.startHrTime;
if (duration > 1000) {
console.warn(`Slow query detected: ${duration}ms`);
}
}
}
});
// DataLoaderの活用でN+1問題を回避
const createLoaders = () => ({
userPostsLoader: new DataLoader(async (userIds) => {
const posts = await db.query(
'SELECT * FROM posts WHERE user_id = ANY($1)',
[userIds]
);
// userIdごとにグループ化して返す
return userIds.map(id => posts.filter(p => p.user_id === id));
})
});
クエリ複雑度の制限
GraphQLではクエリの深さや複雑度に制限を設けることが重要です。攻撃者によるDoS攻撃を防ぎ、スパイク負荷を軽減できます:
// クエリ複雑度スコアを計算
function calculateQueryComplexity(field, complexity = 1) {
// ネストの深さに応じて複雑度を増加
if (field.selectionSet) {
const subComplexity = field.selectionSet.selections.reduce((sum, subField) => {
return sum + calculateQueryComplexity(subField, complexity + 1);
}, 0);
return complexity * 10 + subComplexity;
}
return complexity;
}
// リクエスト時に複雑度をチェック
app.use('/graphql', (req, res, next) => {
const complexity = calculateQueryComplexity(req.body.query);
if (complexity > 5000) {
return res.status(400).json({
error: 'Query too complex. Max complexity: 5000'
});
}
next();
});
よくある質問
一概には言えませんが、一般的に:
実装次第です。DataLoaderの導入、クエリの最適化、インデックスの工夫により、ほぼ回避できます。筆者の経験では、初期段階でDataLoaderを導入することで、90%以上のN+1問題を防げます。
即座の全面移行は推奨しません。段階的なアプローチが現実的です:
いいえ。実務では両者を並用するハイブリッドアプローチが多くあります。シンプルなエンドポイントはREST、複雑なデータ要件はGraphQLという組み合わせが効果的です。
まとめ
- GraphQLはペイロード削減(40~60%)と複数リクエスト排除により、特にモバイル環境で顕著なパフォーマンス向上を実現
- REST APIはHTTPキャッシング活用とシンプルな実装がメリット。単純なデータ構造には最適
- GraphQLのパフォーマンス実現にはDataLoader、クエリ複雑度制限などの最適化が必須
- N+1問題はDataLoader導入により効果的に回避可能
- 実務では、データ複雑性、クライアント多様性、チーム経験を総合的に判断して選択すべき
- ハイブリッドアプローチ(REST + GraphQL)も有効な選択肢
- キャッシング戦略が重要な場面ではREST優位、モバイル効率重視ではGraphQL優位
- 初期段階での適切な実装パターン選定(DataLoader、キャッシング戦略など)が中長期的なパフォーマンスを大きく左右
参考資料: