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、キャッシング戦略など)が中長期的なパフォーマンスを大きく左右

参考資料:

K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →