ZOZOマッチのリアルタイムメッセージング基盤 〜AWS AppSyncとGraphQL Subscriptionの活用〜

ZOZOマッチのリアルタイムメッセージング基盤 〜AWS AppSyncとGraphQL Subscriptionの活用〜

こんにちは、ZOZOの市橋です。2025年6月にリリースされたマッチングアプリであるZOZOマッチのバックエンド開発を担当しています。本記事では、ZOZOマッチのリアルタイムメッセージング機能を実現するために、AWS AppSyncとGraphQL Subscriptionを活用したアーキテクチャと実装について紹介します。

なお、本記事ではバックエンドのアーキテクチャにフォーカスして解説しますが、ネイティブアプリ側の実装については別記事「ZOZOマッチアプリのメッセージ機能を支えるFlutter × GraphQLの実装」で紹介しています。こちらもご確認いただくことでより理解を深められます。

目次

ZOZOマッチシステム全体の構成

本記事で紹介するAWS AppSyncのZOZOマッチシステム内での位置付けを整理するため、まず全体のシステム構成をご紹介します。

ZOZOマッチシステム全体構成図

ZOZOマッチのバックエンドシステムはAWS上に構築されています。アプリケーション実行基盤にはECS(Fargate)、開発言語はJava、フレームワークにはSpring Bootを採用しています。

ZOZOマッチのAPIアクセスはCloudFrontを経由して振り分けられます。CloudFrontでは、パスパターンによって振り分け先を制御しており、GraphQL関連のリクエストのみAppSyncにルーティングされます。

  • REST API: CloudFront → API Gateway → ECS (Java/Spring Boot)
  • GraphQL API: CloudFront → AppSync → データソース(バックエンドAPI)

AWS AppSyncとは

AWS AppSyncは、AWSが提供するフルマネージドGraphQLサービスです。AWS AppSyncを理解するために、まずGraphQLの基本的な知識について説明します。

GraphQLとは

GraphQLは、Facebook(現Meta)が開発したAPI用のクエリ言語およびランタイムです。REST APIの課題を解決するために設計されました。GraphQLには主に3つの操作タイプがあります。

操作 役割 HTTPメソッド相当
Query データ取得 GET
Mutation データ作成・更新・削除 POST/PUT/DELETE
Subscription リアルタイム通知 WebSocket

Queryは必要なフィールドのみを指定して取得できるため、オーバーフェッチングやアンダーフェッチングを防止できます。SubscriptionはWebSocket接続を確立し、サーバー側のイベント発生時にクライアントへ即座にデータを配信します。

AppSyncの基本概念

AWS AppSyncを理解する上で重要な2つの概念、GraphQLスキーマとResolverについて説明します。

GraphQLスキーマ

GraphQLスキーマは、APIの仕様を定義する設計図です。型定義と操作定義で構成されます。

GraphQLでは、IDStringIntBooleanなどの基本的なスカラー型に加えて、独自のカスタム型を定義できます。型名の後ろに感嘆符(!)を付けることで必須項目を表現します。感嘆符がない場合はnullを許容します。配列型は角括弧[]で表現します。感嘆符の位置によって以下のように意味が変わります。

  • [Post] - 配列・要素とともにnull許容
  • [Post!] - 配列はnull許容、要素はnull不可
  • [Post]! - 配列はnull不可(空配列は可能)、要素はnull許容
  • [Post!]! - 配列・要素とともにnull不可

以下は型定義の例です。

type Post {
  id: ID!
  title: String!
  content: String!
  status: PostStatus!
  author: User!
}

type User {
  id: ID!
  name: String!
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

次に、操作定義(Query、Mutation、Subscription)の例を示します。GraphQLでは、type Querytype Mutationtype Subscriptionという特別な型で、クライアントが実行できる操作を定義します。各操作には引数と戻り値の型を指定できます。引数に!を付けると必須、付けないと任意になります。

type Query {
  getPost(id: ID!): Post
  listPosts(limit: Int): [Post]
}

type Mutation {
  createPost(title: String!, content: String!): Post
  updatePost(id: ID!, title: String, content: String): Post
}

type Subscription {
  onPostCreated: Post
  @aws_subscribe(mutations: ["createPost"])
}

Resolver

Resolverは、GraphQL操作とデータソースを接続する橋渡しの役割を果たします。AppSyncでは、以下をデータソースとして接続できます。*1

  • HTTP API
  • DynamoDB
  • Lambda
  • RDS
  • OpenSearch
  • Amazon EventBridge

ZOZOマッチでは、これらの選択肢の中からHTTP APIをデータソースとして採用しています。詳しい理由については後述の利用シーンで説明します。

Resolverでは、VTL(Velocity Template Language)またはJavaScriptを使用してGraphQLリクエストとデータソース間でデータを変換します。本記事ではVTLを使用した実装を紹介します。この変換は、Request Mapping TemplateとResponse Mapping Templateの2つで構成されます。

  • Request Mapping: GraphQL引数をデータソース固有のリクエスト形式に変換する。パラメータの追加や変換、認証ヘッダーの付与、リクエストメソッドやパスの指定などを行う
  • Response Mapping: データソースのレスポンスをGraphQLスキーマに適合する形式へ変換する。データ構造の整形、エラーハンドリング、不要なフィールドのフィルタリングなどを行う

Request Mapping Templateの例は以下の通りです。VTLの条件分岐やユーティリティ関数を使用してデータを変換します。

## リクエストボディの構築
#set($body = {})
$util.qr($body.put("title", $ctx.args.title))
$util.qr($body.put("content", $ctx.args.content))

## オプショナルなパラメータの処理
#if($ctx.args.tags)
  $util.qr($body.put("tags", $ctx.args.tags))
#end

## 認証情報の追加
#set($userId = $ctx.identity.sub)
$util.qr($body.put("author_id", $userId))

{
  "version": "2018-05-29",
  "method": "POST",
  "resourcePath": "/api/posts",
  "params": {
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "$ctx.request.headers.Authorization"
    },
    "body": $util.toJson($body)
  }
}

Response Mapping Templateの例は以下の通りです。

#if($ctx.result.statusCode == 200)
  $util.toJson($ctx.result.body)
#else
  #set($response = $util.parseJson($ctx.result.body))
  #if($response.type)
    #set($errorType = $response.type)
  #else
    #set($errorType = "none")
  #end
  $util.error("Error response received. statusCode: $ctx.result.statusCode, type: $errorType, message: $response.message")
#end

なぜZOZOマッチでGraphQLを採用したのか

GraphQLとAppSyncの基本概念を整理したところで、本章ではZOZOマッチがなぜAppSyncを採用したのか、その技術選定の背景と理由を説明します。

マッチングアプリとしてのZOZOマッチの要件を整理し、別のアプローチと比較しながら、GraphQL Subscriptionがどのように課題を解決するかを解説します。

マッチングアプリの要件

ZOZOマッチでは、チャットメッセージの送受信や未読件数の同期において、相手からの反応を即座に伝えるリアルタイム性が求められます。また、同時接続ユーザー数の増加に対応できるスケーラビリティも重要な要件です。

別のアプローチとして、クライアントが定期的にサーバーへHTTPリクエストを送信するポーリング方式が考えられますが、この方式には以下のような課題があります。

  • ポーリング間隔分の遅延が発生する
  • 即時性とコストのトレードオフ(ポーリング間隔を短縮すれば遅延は改善するもののコストが増加し、延長すればコストは削減できるものの遅延が悪化する)が発生する
  • 更新がないケースでも常時ポーリングが発生するため、サーバーリソースの無駄が多い
  • アクティブユーザー数に比例して負荷が増加するため、スケーラビリティに懸念がある

これらの課題に対し、WebSocket接続を確立してサーバー側のイベント発生時にクライアントへ即座にデータを配信するGraphQL Subscriptionを採用しました。

WebSocket通信の仕組みと利点

WebSocketは、クライアントとサーバー間で双方向の持続的な接続を確立し、リアルタイムにデータをやり取りするプロトコルです。WebSocketの利点としては以下が挙げられます。

  • 双方向通信: サーバー・クライアント双方からデータを送信でき、クライアントからのリクエストを待たずにサーバーからプッシュできる
  • 低レイテンシ: 接続確立後はハンドシェイク不要のため、イベント発生から配信までの遅延が最小限
  • 低オーバーヘッド: 接続確立後はHTTPヘッダの送信が不要のため、データ転送量を削減できる
  • スケーラビリティ: 1つの接続で複数のメッセージをやり取りできるため、接続数を抑制できる

WebSocketによるリアルタイム通信は以下のような流れで行われます。

WebSocket通信のシーケンス図

まず、クライアントがサーバーに対してWebSocketハンドシェイク(HTTP Upgradeリクエスト)を送信し、WebSocket接続を確立します。この接続は一度確立されると、明示的に切断されるまで持続的に維持されます。その後、サーバー側で何らかのイベントが発生すると、サーバーは接続済みのクライアントに対して即座にデータを送信できます。クライアントはリアルタイムにデータを受信し、画面を更新します。この仕組みにより、クライアントからのリクエストを待つことなく、サーバー側でイベントが発生すると即座にデータを配信できます。

ZOZOマッチにおけるGraphQL Subscriptionの活用

ここまで、GraphQLの基本概念、AppSyncのスキーマとResolver、そしてWebSocketによるリアルタイム通信の仕組みについて説明してきました。

この章では、これらの技術要素をどのように活用しているのか、ZOZOマッチの具体的な実装例を交えて紹介します。

なお、ZOZOマッチではデータソースとしてHTTP APIを利用しており、ビジネスロジックやバリデーション等を一元管理するバックエンドAPIを設けています。弊チームではDDDを採用しており、ドメイン層にビジネスロジックを集約することで、AppSyncのResolverは薄いインタフェース層として保っています。これにより、バックエンドAPIとして独立したテストが可能になり、既存の運用ノウハウも活用できます。

利用シーン1: メッセージ送信と受信通知

概要

ZOZOマッチでは、マッチングした相手とチャットでメッセージのやり取りができます。このチャット機能において、送信したメッセージを相手の画面にリアルタイムで表示するために利用します。

処理の流れ

メッセージ送受信の処理は以下の3つのステップで構成されます。

利用シーン1のシーケンス図

まず、チャット画面を開いた時にクライアントがそのチャンネルのchannelIdを指定して、AppSyncに対してonMessageModified Subscriptionを開始します。これにより、WebSocket接続が確立され、そのチャンネルのメッセージ更新のみをリアルタイムで受信できる状態になります。

次に、ユーザーがメッセージを送信すると、クライアントからcreateMessage Mutationが実行されます。AppSyncはHTTP Resolverを通じてECS上のREST APIを呼び出し、APIがデータベースにメッセージを保存してチャンネル情報を更新します。処理が完了すると、AppSyncはMutationの結果をクライアントに返却します。

最後に、AppSyncはcreateMessage Mutationの実行を検知します。そして、そのチャンネルのonMessageModified Subscriptionを購読しているすべてのクライアント(送信者と受信者の両方)に対して、自動的にメッセージ更新を配信します。これにより、受信者の画面にリアルタイムでメッセージが表示されます。

実装内容

それでは、GraphQLスキーマとVTL Resolverの実装を見ていきます。特に、Subscriptionのトリガー設定と、GraphQLからREST APIへの変換ロジックに注目してください。

GraphQLスキーマ:

type Mutation {
  # メッセージ送信
  createMessage(channelId: String!, kind: String!, body: String!): MessageModifiedResponse
  @aws_cognito_user_pools
}

type Subscription {
  # メッセージ変更をリアルタイム受信
  onMessageModified(channelId: String!): MessageModifiedResponse
  @aws_subscribe(mutations: ["createMessage"])
  @aws_cognito_user_pools
}

type MessageModifiedResponse {
  channelId: String!
  action: String!
  message: Message!
}

type Message {
  channelId: String!
  sentAt: String!
  senderUserId: String!
  data: String!
}

onMessageModified@aws_subscribe(mutations: ["createMessage"])ディレクティブでは、createMessage Mutationが実行されるとトリガーされます。これにより、channelIdonMessageModified Subscriptionを購読しているクライアントへデータが配信されます。つまり、あるチャンネルでメッセージが送信されると、そのチャンネルを購読しているクライアントへ即座に通知が届きます。

また、このスキーマでは、@aws_cognito_user_poolsディレクティブによって、Cognito User Poolsで認証されたモバイルアプリのみがこの操作をできるように制限しています。

VTL Resolverの実装

ZOZOマッチでは、VTLを使用してGraphQL MutationをBackend REST APIへのHTTPリクエストに変換しています。なお、バックエンドAPIのエンドポイントURLは、AppSyncのHTTP Data Sourceに事前設定されており、VTLではresourcePathのみを指定します。

Request Mapping Template:

{
  "version": "2018-05-29",
  "method": "POST",
  "resourcePath": "/messages",
  "params": {
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "$ctx.request.headers.Authorization"
    },
    "body": {
      "channel_id": "$ctx.args.channelId",
      "message_body": "$ctx.args.body",
      "sender_user_id": "$ctx.identity.sub"
    }
  }
}

Response Mapping Template:

#if($ctx.result.statusCode == 200)
  {
    "channelId": "$ctx.result.body.channel_id",
    "action": "$ctx.result.body.action",
    "message": {
      "channelId": "$ctx.result.body.message.channel_id",
      "sentAt": "$ctx.result.body.message.sent_at",
      "senderUserId": "$ctx.result.body.message.sender_user_id",
      "data": "$ctx.result.body.message.data"
    }
  }
#else
  #set($response = $util.parseJson($ctx.result.body))
  #if($response.type)
    #set($errorType = $response.type)
  #else
    #set($errorType = "none")
  #end
  $util.error("Error response received. statusCode: $ctx.result.statusCode, type: $errorType, message: $response.message")
#end

利用シーン2: 未読件数のリアルタイム同期

概要

続いてはチャンネルの未読件数をリアルタイムで同期する仕組みです。メッセージを受信した際に未読バッジが即座に増え、メッセージを読んだ際に未読バッジが即座に消えます。

処理の流れ

ここでは、メッセージを既読にして未読バッジが消えるケースを例に説明します。なお、メッセージ受信時に未読件数が増える場合も、同じonChannelModified Subscriptionを通じて通知されます。

未読件数の同期処理は以下の3つのステップで構成されます。

利用シーン2のシーケンス図

まず、ユーザーのアプリがAppSyncに対してonChannelModified Subscriptionを開始します。これにより、チャンネル情報の更新をリアルタイムで受信できる状態になります。

次に、ユーザーがメッセージを既読にすると、アプリからAppSyncに対してclearUnreadCount Mutationを実行します。AppSyncはHTTP Resolverを通じてバックエンドのREST APIを呼び出し、データベースの未読件数を更新します。

最後に、AppSyncはclearUnreadCount Mutationの実行を検知します。その後、onChannelModified Subscriptionを購読しているクライアント(この場合はユーザー)に対して、自動的にチャンネル更新情報を配信します。ユーザーのアプリは受信したチャンネル更新情報をもとに、画面上の未読バッジを更新します。

実装内容

これを実現する実装は以下の通りです。

GraphQLスキーマ:

type Mutation {
  # 未読件数をクリア
  clearUnreadCount(partnerUserId: String!): Channel
  @aws_cognito_user_pools
}

type Subscription {
  # チャンネル更新を受信
  onChannelModified(userId: String!): Channel
  @aws_subscribe(mutations: ["clearUnreadCount"])
  @aws_cognito_user_pools
}

type Channel {
  userId: String!
  partnerUserId: String!
  unreadCount: Int!
  messageContents: MessageContents!
}

type MessageContents {
  sentAt: String!
  data: String!
}

@aws_cognito_user_poolsにより、Cognitoで認証されたモバイルアプリのみがclearUnreadCount の実行とonChannelModified を購読できます。

onChannelModified@aws_subscribe(mutations: ["clearUnreadCount"])ディレクティブにより、clearUnreadCount Mutationが実行されます。その後、該当するuserIdonChannelModified Subscriptionを購読しているクライアントへチャンネル更新情報が配信されます。

VTL Resolverの実装:

VTLを使用してclearUnreadCount MutationをバックエンドREST APIへのHTTPリクエストに変換します。

Request Mapping Template:

#set($userId = $ctx.identity.sub)
#set($partnerUserId = $util.urlEncode($ctx.args.partnerUserId.trim()))

{
  "version": "2018-05-29",
  "method": "POST",
  "resourcePath": "/channels/unread-count/reset",
  "params": {
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "$ctx.request.headers.Authorization"
    },
    "body": {
      "user_id": "$userId",
      "partner_user_id": "$partnerUserId"
    }
  }
}

Response Mapping Template:

#if($ctx.result.statusCode == 200)
  $util.toJson($ctx.result.body)
#else
  #set($response = $util.parseJson($ctx.result.body))
  #if($response.type)
    #set($errorType = $response.type)
  #else
    #set($errorType = "none")
  #end
  $util.error("Error response received. statusCode: $ctx.result.statusCode, type: $errorType, message: $response.message")
#end

利用シーン3: バックエンドからのリアルタイム通知

概要

ZOZOマッチでは、ユーザーが送信するメッセージの一部は審査を経て相手に表示されます。審査を要する場合、ユーザーがチャットメッセージを送信すると、審査通過後にバックエンドサービス(SQS Consumer)がメッセージとチャンネルデータを非同期で更新します。この場合、AppSyncのMutationを経由しないため、Subscriptionはトリガーされません。この問題を解決するため、データ更新後にnotifyChannelUpdate Mutation(IAM認証)を別途呼び出すことで、Subscriptionだけを明示的にトリガーしています。

以下の構成図は、この一連の処理に関わるAWSサービスとデータフローを示しています。

notifyChannelUpdate構成図

処理の流れ

チャットメッセージ審査通過時のチャンネル更新処理は以下の4つのステップで構成されます。

利用シーン3のシーケンス図

まず、SQS Consumerがチャットメッセージ審査通過イベントを取得します。

次に、バックエンドサービス(ECS/Fargate)がメッセージとチャンネルデータをDynamoDBに書き込みます。

その後、バックエンドサービスがAppSyncに対してnotifyChannelUpdate Mutation(IAM認証)を実行します。この時点でデータベースの更新は完了しており、Subscriptionのトリガーのみを目的としています。

最後に、AppSyncはnotifyChannelUpdate の実行を検知し、onChannelModified を購読しているクライアントに対して、チャンネル更新情報を配信します。

実装内容

実装はこちらです。

GraphQLスキーマ:

type Mutation {
  # バックエンド専用: Subscription配信トリガー
  notifyChannelUpdate(
    userId: String!,
    partnerUserId: String!
  ): Channel
  @aws_iam

  # アプリ用: 未読件数をクリア
  clearUnreadCount(partnerUserId: String!): Channel
  @aws_cognito_user_pools
}

type Subscription {
  # チャンネル更新を受信
  onChannelModified(userId: String!): Channel
  @aws_subscribe(mutations: ["clearUnreadCount", "notifyChannelUpdate"])
  @aws_cognito_user_pools
}

type Channel {
  userId: String!
  partnerUserId: String!
  unreadCount: Int!
  messageContents: MessageContents!
}

type MessageContents {
  sentAt: String!
  data: String!
}

notifyChannelUpdate Mutationには@aws_iamディレクティブが付与されており、IAM認証によるアクセスのみを許可しています。これにより、バックエンドサービス(ECS/Fargate)からのみ実行可能となり、モバイルアプリから直接呼び出すことはできません。

Channel型は利用シーン2と同じ構造で、バックエンドから配信されるチャンネル更新情報の形式を定義しています。

このように、AppSyncのMutationを経由しない処理でも、データ更新完了後にnotifyChannelUpdateを呼び出すことで、リアルタイム通知を実現しています。

利用シーン4: チャンネル・メッセージ一覧取得

概要

チャット画面を開いた際に、過去のメッセージ履歴を取得します。この機能では、サーバーからのリアルタイムなプッシュ配信が不要なため、SubscriptionではなくGraphQL Queryを使用します。

GraphQL Queryを使用することで、クライアントが必要なフィールドのみを指定して取得できます。これにより、REST APIで発生しがちなオーバーフェッチング(不要なデータまで取得してしまう)やアンダーフェッチング(必要なデータが不足して追加リクエストが必要になる)を防止できます。また、AppSyncがバックエンドのREST APIをラップすることで、BFF(Backend For Frontend)のような役割を果たし、クライアントにとって最適なデータ形式を提供できます。

処理の流れ

メッセージ一覧取得の処理はシンプルな1ステップで構成されます。

利用シーン4のシーケンス図

クライアントがチャット画面を開くと、AppSyncに対してlistMessages Queryを実行します。AppSyncはHTTP Resolverを通じてECS上のREST APIを呼び出し、APIがデータベースから指定されたチャンネルのメッセージ一覧を取得します。取得したメッセージはAppSyncを経由してクライアントに返却され、画面に表示されます。

実装内容

それでは実装を見ていきます。

GraphQLスキーマ:

type Query {
  # メッセージ一覧取得
  listMessages(channelId: String!, limit: Int): [Message]
  @aws_cognito_user_pools
}

type Message {
  sentAt: String!
  senderUserId: String!
  data: String!
}

このスキーマでは、@aws_cognito_user_poolsディレクティブによって、Cognitoで認証されたモバイルアプリのみにlistMessages Query実行を制限しています。

VTL Resolverの実装:

AppSyncはHTTP Resolverを使用してバックエンドAPIを呼び出し、メッセージ一覧を取得します。

Request Mapping Template:

{
  "version": "2018-05-29",
  "method": "GET",
  "resourcePath": "/channels/$ctx.args.channelId/messages",
  "params": {
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "$ctx.request.headers.Authorization"
    },
    "query": {
      #if($ctx.args.limit)
        "limit": "$ctx.args.limit"
      #end
    }
  }
}

Response Mapping Template:

#if($ctx.result.statusCode == 200)
  #set($messages = [])
  #foreach($item in $ctx.result.body.items)
    #set($message = {
      "sentAt": $item.sent_at,
      "senderUserId": $item.sender_user_id,
      "data": $item.data
    })
    $util.qr($messages.add($message))
  #end
  $util.toJson($messages)
#else
  #set($response = $util.parseJson($ctx.result.body))
  #if($response.type)
    #set($errorType = $response.type)
  #else
    #set($errorType = "none")
  #end
  $util.error("Error response received. statusCode: $ctx.result.statusCode, type: $errorType, message: $response.message")
#end

運用上の課題

ここまで、ZOZOマッチにおけるGraphQL Subscriptionの具体的な実装例を紹介してきました。ここからは実際に運用する中で直面した課題と、それらに対する対応について紹介します。

学習コスト

GraphQLは、REST APIとは異なるパラダイムであり、新たに概念を学ぶ必要があります。GraphQLのスキーマ設計やResolver実装、Subscriptionの仕組みなど、学ぶべき概念が多く、初期の学習コストが高いという課題があります。特にResolverの実装でVTL(Velocity Template Language)を利用する場合は、独自のテンプレート言語であるため、独特の記法に対して一定の慣れが必要です。

現状では、GraphQL周りの知見が一部のメンバーに属人化している状況です。そのため、今後は有識者が主導して勉強会を開催し、チームメンバー全体で仕組みを把握することを検討しています。

モニタリングの難しさ

GraphQLでは、すべての操作が単一のエンドポイントに集約されるため、REST APIで培ったモニタリングの知見を直接的には活かせません。REST APIでは、エンドポイントごとにエラー率やレイテンシをモニタリングすることで影響範囲を迅速に特定できます。一方、GraphQLではエンドポイントが1つしかないため、デフォルト設定ではどのGraphQL操作で問題が起きているのかを特定することが困難です。

この課題に対しては、AppSyncのEnhanced Metricsを有効化することで対応しました。具体的には、以下の3つのレベルでメトリクスを取得しています。

  • Operation Level Metrics: Query/Mutation/Subscription単位でレイテンシやエラー率をモニタリング
  • Resolver Level Metrics: 各Resolver単位でパフォーマンスを追跡
  • Data Source Level Metrics: バックエンドAPI呼び出しの状況をモニタリング

これらのメトリクスはCloudWatch Metric Stream経由でDatadogに送信され、Operation単位でのパフォーマンス分析が可能になっています。これにより、問題が発生した際に、どのGraphQL操作のどのResolverで問題が起きているのかを特定しやすくなります。

ローカル開発環境の制約

AppSyncはAWSのマネージドサービスであるため、ローカル開発環境の整備に工夫が必要です。LocalStackのUltimateプランなど、AppSyncをローカルで再現できるツールも存在しますが、コストや運用面での制約があるため、ZOZOマッチでは現状これらのツールを採用していません。

そのため、GraphQLスキーマやVTL Resolverの動作を確認するには、実際にAWS環境にデプロイしてリクエストを送信する必要があります。これにより、ちょっとした修正でもAWS環境での確認が必要になるため、手間が発生します。また、開発者ごとに独立したAWS環境を用意することが現状はできていないため、共有の環境を使用せざるを得ず、他の作業と競合する場面もあります。

この課題に対しては根本的な解決策は見つかっていません。軽減策として、可能な限りバックエンドAPIのロジックを充実させ、AppSyncのResolverはシンプルに保つことで、AWS環境への依存を最小限にしています。また、GraphQLスキーマの変更は慎重に行い、事前にチーム内でレビューを実施することで、デプロイ後の手戻りを減らすよう努めています。ただ、最終的にはAWS環境での検証が不可欠であるため、現在も開発サイクルの長さは課題として残っています。

まとめ

本記事では、ZOZOマッチのリアルタイムメッセージング機能を実現するために採用したAWS AppSyncとGraphQL Subscriptionのアーキテクチャと実装について紹介しました。

実際の利用シーンを通じて、Mutation、Subscription、Queryの使い分けや、VTL Resolverを用いたGraphQLとREST APIの変換方法を解説しました。これらの実装により、ZOZOマッチは低レイテンシかつスケーラブルなリアルタイムメッセージング基盤を実現しています。

本記事が、同様のシステム開発を検討している方の参考になれば幸いです。

さいごに

ZOZOでは、ZOZOTOWN一本足打法からの脱却を狙い、新規事業にも果敢に取り組んでいます。このような挑戦を一緒に楽しめる仲間を募集しています。ご興味のある方は、採用ページをご覧ください。

hrmos.co

カテゴリー