こんにちは、estieでソフトウェアエンジニアをしている木村です。
今回は私がテックリードを務めている「estie 物流リサーチ」というプロダクトにおいて、RESTからGraphQLに「迅速」かつ「安全」に移行した話をします。
移行のタイムラインとしては以下の通りで、既存の開発も進めつつ二人のメンバーで約2.5ヶ月という期間で完全に移行することができました。
2024/08/14 | 移行の計画と開始 |
2024/10/31 | GraphQLへの完全切り替えの完了 |
2024/11/21 | 移行後の片付けの完了 |
今回はいかにして少人数で安全に素早く移行できたかについてその工夫を紹介していきます。
なぜ移行したのか?
「estie 物流リサーチ」は社内では DaaS(Data as a Service)と呼ばれており、物流施設の情報を良い感じに検索・表示・分析するためのアプリケーションです。
GraphQLは各画面で必要なデータを柔軟に取得でき、不動産データが「物件」を軸としたリレーショナルな構造になっている点からも、非常にDaaSと相性が良いです。実際、estieでは他のほとんどのプロダクトがGraphQLを採用しており、社内標準となっています。
そのなかで、「estie 物流リサーチ」は元々の歴史が長かった「estie マーケット調査」からforkしてできたプロダクトであり、マーケット調査がGraphQLを採用する前だったため、その負債を引き継いでいました。具体的には以下の問題が発生していました。
オーバーフェッチ
各ページで必要なデータが少しずつ異なる場合でもエンドポイントを使い回していたため、それぞれのページでカラムが追加されるにつれオーバーフェッチが発生していました。
型の不整合
REST APIのレスポンスに適切な型を適用する仕組みがフロントエンド側で整っておらず、バックエンドの型定義との不整合が発生し、不具合が生じていました。例えば、バックエンドで複数形のフィールド名として定義されていたものがフロントエンドでは単数形でtypoしていたり、フロントエンドで他のエンドポイントで使われている型が使いまわされていて、実際にはデータが返っていなかったりといったバグが生じていました。
パフォーマンスの問題
オーバーフェッチによるバックエンド、フロントエンド双方でのパフォーマンス低下に加え、様々な場所でN+1クエリが発生していました。GraphQLの採用によりDataloaderを活用し、一箇所でN+1問題を解決できることを期待しました。
プロダクト間での知見活用
社内標準としてGraphQLが採用されている中でREST APIを使い続けており、他プロダクトの知見を活用できない状況でした。加えて、社内のinternal APIでもGraphQLが採用されており、物流チーム内に知見が少なく、社内APIの活用がしづらい状態にありました。
移行計画
私は移行において、以下2点を考えることが非常に重要だと考えています。
移行期間中の開発はどのように進めるか
移行後の環境が正しく動いていることをどのように検証するか
例えば、1については「移行期間中は既存の開発を完全に停止する」という選択肢もありますし、2については「手動テストを徹底する」ことがプロダクトの規模によっては最適解になる場合もあります。移行計画の段階でこの2点を明確にしておかないと、移行はうまくいきません。
今回の移行では 「GraphQL Switch」という仕組みを考え
RESTとGraphQLを共存させ段階的に切り替え
RESTとGraphQLのデータが完全に一致していることを機械的に検証する仕組みを導入(+簡単なUIのQA)
というアプローチをとりました。
特に「GraphQLのデータがRESTと完全に一致していることを機械的に検証する仕組み」が重要で、はじめからGraphQLで最適化されたクエリを作るのではなく、まずはオーバーフェッチを許容してRESTと全く同じレスポンスを返すようにしそれを検証、その後、GraphQLに完全に切り替えた後にオーバーフェッチを解消する、という二段階の移行ステップを取りました。
GraphQL Switch
GraphQL Switchの簡単なコンセプトは以下の図です。
フロントエンド側からGraphQLとRESTの両方のリクエストを投げ、それをGraphQL Switchに渡します。ユーザーには今まで通りRESTのレスポンスを返しつつも、GraphQLのレスポンスと裏で検証します(いわゆるShadow Testingと呼ばれている手法です)。検証の際にはGraphQLのレスポンスを加工しRESTのレスポンスと同じ形に整形した上で比較し、データに差分がある場合にはSentryに通知します。
リソースのまとまりごとにFeature Toggleで切り替えられるようにしており、検証やQAが完了した後にFeature Toggleをオンにすることで、段階的にGraphQLへ切り替えていきました。
以下は、実際に導入していたTypeScriptのhookのコードのイメージです。(実際にはfetchingの状態を見ていたり、特定のカラムは無視できるようにしていたりなどもう少し細かな制御がありますが、概ねこのようなコードです)
import { detailedDiff } from 'deep-object-diff'; export const useGraphqlSwitch = (key: ResourceKey) => { const { toggles } = useFeatureToggles(); const isCheckEnabled = !!toggles[`graphql-check-${key}`]; const isSwitchEnabled = !!toggles[`graphql-release-${key}`]; const isGraphqlDisabled = !isCheckEnabled && !isSwitchEnabled; const querySwitch = useCallback( <T extends object>(restResponse: Response<T>, graphqlResponse: Response<T>): Response<T> => { if (isGraphqlDisabled) { return restResponse; } if (isCheckEnabled) { const diff = detailedDiff(restResponse, graphqlResponse); if (Object.keys(diff.added).length || Object.keys(diff.deleted).length || Object.keys(diff.updated).length) { captureException(new Error(`GraphQL data ${key} is different from REST response`), { extra: { graphqlResponse, restResponse, diffAdded: diff.added, diffDeleted: diff.deleted, diffUpdated: diff.updated, }, }); } } return isSwitchEnabled ? graphqlResponse : restResponse; }, [isGraphqlDisabled, isCheckEnabled, isSwitchEnabled, key] ); return { isGraphqlDisabled, querySwitch }; };
このように、移行中はRESTのレスポンスを返しつつ、裏でGraphQLの検証を行うことで、安全に移行を進められます。ユーザーからの実リクエストを通して十分な検証ができた後に切り替えることで、リスクを最小限に抑えることができます。
GraphQL Switchを使う場合に考慮すべき点
GraphQL Switchを使う場合にはいくつか気をつけなくていはいけない点があります。
サーバーの負荷
GraphQL SwitchではRESTとGraphQLの両方のリクエストを検証・移行期間中に送信するため、サーバーやDBの負荷が増加します。今回はその点は考慮し警戒しつつも、サーバー的にもDB的にも余裕がありそうだったので特にインフラ側の変更は行いませんでした。しかし、リクエスト数によっては事前にインフラの調整もする必要があります。
フロントエンドへの影響
フロントエンド側への影響は2点あり、一つ目はGraphQLリクエストや検証は裏で行うとはいえ、多少パフォーマンスに影響が出るため、それが許容できるかを判断する必要があります。二つ目はレスポンスが全く変わっていなくとも、GraphQL Switchを挟んだことで挙動が変わる可能性があるという点です。特に、私たちの場合は既存コードのuseEffect
の使い方に問題がある部分があり、GraphQL Switchを挟んだことで挙動が変わったものがいくつかありました。これはUI側のQAでキャッチすることができました。
ログの管理
移行期間中はRESTとGraphQLの両方のログが送られてしまうため、ログを利用した分析を行なっている場合には、ログ切り替えの計画を考慮する必要があります。今回は、切り替え期間中はREST / GraphQLどちらのレスポンスをGraphQL Switchで返している場合でも常に両方のリクエストを送るようにしていました。そのため、完全切り替えを行うまではRESTのログを参照し、完全切り替え後はGraphQLのログを参照することで、ログの重複を避けることができました。
GraphQL Switchの良かった点・苦労した点
良かった点
GraphQL Switchを導入したことで、狙い通り安全に移行を進めることができました。特にレスポンス比較の仕組みにより、検証が楽になっただけでなく、比較しながら移行を進めることでコードの移行スピードも向上しました。私たちのプロダクトではmutationに比べてqueryが多いため、GraphQL Switchで差分が出ていなければほぼ動作が保証されるという点も、この移行方法が有効だった理由の一つかもしれません。
また、差分検証によって既存のバグを多数発見することができ、移行作業と並行して修正することができたのも大きな収穫でした。
重要なのは、一見手間のかかるこの移行手順が、結果として開発のスピード向上につながった点です。
苦労した点
一方で、GraphQL移行の切り替え途中のエンドポイント集を編集する場合、REST側とGraphQL側の両方を編集する必要があり、移行期間中のコードの二重管理はこの移行方法のデメリットと言えるかもしれません。ただ、GraphQL Switchの仕組みによって既存の開発を進めた場合でもGraphQL側を忘れず編集ができるという点では良かったとも思っています。
他に細かいですが苦労した点としては、GraphQLクライアントのキャッシュやそのタイミングによる差分で、既存のコードの楽観的更新をしている部分と移行後のurqlのドキュメントキャッシュで更新されるタイミングが異なるものの調整が必要でした。今回は簡単のためquery側のGraphQL Switchの話のみを具体的に紹介しましたが、urqlのドキュメントキャッシュが正しく更新されるためにはmutationの移行も同時に行う必要があり、mutation側でもGraphQL Switchの概念を適用しました。具体的には、RESTとGraphQL両方のmutationを送り、バックエンド側でFeature Toggleの状態に応じて適用するものを選択していました。
まとめ
GraphQLへの移行により、型の不具合が解消され、データの一貫性が向上しました。
GraphQLスキーマによって型が保証されることで、バックエンドとフロントエンドのデータのずれがなくなり、型の不整合によるバグが発生しなくなりました。
また、バックエンドのロジックが大幅にシンプルになり、データ取得の責務がフロントエンドに寄ったことで、開発の自由度が増しました。その結果、フロントエンド・デザインエンジニアが一貫した開発を行えるようになり、バックエンドの調整を待たずにフロント側の実装を進めることが可能になりました。
この移行によって、開発チーム全体の生産性が向上し、新機能の開発や改善がスピーディーに進められるようになりました。このあたりはこちらでも詳しく話しています。