hey で STORES(EC)や STORES レジのバックエンドエンジニアをしております、知花です。
先日“hey Talk” Engineers 新プロダクト「STORES レジ」を支えるエンジニアリングというイベントにて「GraphQL スキーマで支えるレジアプリ開発」というタイトルで、主に GraphQL スキーマを利用した開発の進め方について話しました。
今回はそのときに話した内容について書いていきます。
はじめに
STORES レジではアプリ用の API として GraphQL を採用しました。API は EC(Rails)のコードベースに乗る形で実装しており、実装には graphql-ruby gem を利用しました。
そもそもなぜ GraphQL を採用したのかについてですが、プロジェクトが始まってまだ開発が始まっていないときに、アプリチームから「GraphQL 使ってみたいんですがどうですか」といった話を受けて検討を始めたのがきっかけでした。
その当時は EC で開発されている各種 API はスキーマが管理されていなかったため、フロントエンドエンジニアとバックエンドエンジニアで共同して作業を進める際は口頭やドキュメントを書いて API スキーマを伝え合うということをしていました。
そのため API スキーマについてのやりとりが頻繁に発生したり、伝えた情報と実装の間に乖離が生じたりするなど、コミュニケーションコストがかかっていた印象がありました。現在この問題は解消されていて、EC では新たに API を開発する際には OpenAPI によるスキーマ定義が必ず行われるようになっています。
他にも GraphQL であることによるメリットやデメリット・実装難度はどうか・テストはどう書いていくのか・監視はどうするのか…などいくつかの観点で検討した上で、利用するにあたって大きな問題はなさそうだと判断し、挑戦してみようということで採用することになりました。
読書会
STORES レジの開発が始まる前に初めての GraphQLという書籍の読書会を行いました。 GraphQL に関する開発をしたことのないメンバーが多かったので、このタイミングで GraphQL についての認識を合わせられたのは良かったです。
余談ですが、読書会で話していくうちに一部のメンバーから「認識のズレが起こって仲が悪くなる」といった、チーム開発でのつらい思い出が話されるなどしたこともあり、そうはならないように開発できるといいなと思いました。
API スキーマの設計・共有方法
STORES レジでの API スキーマの設計・共有方法はおおまかに以下の手順で行いました。
1. バックエンドチームで API のスキーマを設計・実装する
スキーマファーストな開発ではバックエンドエンジニアとフロントエンドエンジニアで API スキーマについての議論・定義を先に行い、スキーマ定義を固めた上で両者の開発を並行して進めていくというのが一般的で、EC でもこの方法で開発しています。
ですが、STORES レジの開発では始めにバックエンドチームによって API が実装され、その間アプリチームは並行して UI 部分を実装する方法をとっていました。 このようにしていた理由としては2点あります。
1つ目の理由は、graphql-ruby は実装からスキーマを生成するので実装から始めるのが楽という点です。
graphql-ruby を使った開発ではスキーマ定義と実装が同時にできるので、スキーマ定義にかかる時間を減らすことができたように思います。 とは言え先にスキーマの定義から行うことはできないかというとそうでもなさそうで、こちらについては後述します。
2つ目の理由は、レジアプリ用 API が EC のコードベースに乗る形で実装していたという点です。
アプリチームはこれまで EC に関わったことがなく、EC のデータや仕様について知っているバックエンドチームでスキーマの設計をすると効率が良いのでは、ということでこのような方法をとっていました。 もちろん API について不明点があれば適宜コミュニケーションをとったり、アプリチームからのフィードバックを受けて実装やスキーマを変更することもありました。 また両チームとも要件定義段階から関わっており、レジアプリの仕様についてもドキュメントが書かれていたので、認識のズレが起こって仲が悪くなるみたいなことはなかったです。
2. Rake タスクによって実装からスキーマを生成する
API の実装を終えたら Rake タスクによってスキーマを生成しますが、そのための仕組みは graphql-ruby に用意されているので、Rake タスクの定義は以下のコードのみで済みます。
require 'graphql/rake_task' GraphQL::RakeTask.new(schema_name: 'MySchema')
これによって graphql:schema:idl
等のスキーマ生成タスクが定義でき、実行すれば実装と乖離のないスキーマファイルの生成ができます。
スキーマ生成タスクの実行が漏れてしまうと実装とスキーマに乖離が生じる恐れはありますが、graphql-ruby のドキュメントを参考にした以下のテストで防ぐようにしました。
このテストでは実装から生成されるスキーマと現在のスキーマファイルの内容を比較して差分がないことを検証しています。 実行が漏れていた場合は CI で検知できるので、アプリチームには常に新鮮な API スキーマをお届けできます。
RSpec.describe Schema do let(:current_schema) { Schema.to_definition } let(:previous_schema) { File.read(Rails.root.join('app/graphql/schema.graphql')) } it 'スキーマの変更が schema.graphql に反映されている' do expect(current_schema).to eq previous_schema end end
3. アプリチームにスキーマを共有する
上記の Rake タスクで生成されたファイルを repository に commit しておき、アプリチームはこのファイルを参照して API を利用した開発を進める...という方法をとりました。
アプリチーム側の repository でも別途スキーマを管理していますが、アプリチームではこれらのスキーマファイルに差分が生じている場合は CI で差分の内容を Slack に通知する仕組みを導入しています。 この仕組みによりアプリチームに API の変更内容を詳細に伝えずとも把握してもらえるようになりました。
スキーマ定義から始めることはできないか
先述したスキーマを最初に定義する方法についてですが、graphql-ruby の実装の過程をスキーマ定義部分とリゾルバ(実際に返す値を生成する関数)実装部分に分割すれば実現できるのではないかと考えています。
以下が graphql-ruby を利用した実装の一部で、 field ...
となっている行によってスキーマ定義が行われます。
class ArticleType < Types::BaseObject field :title, String, null: false, description: 'ブログタイトル' field :thumbnail_url, String, null: true, description: 'サムネイル画像 URL' field :created_at, GraphQL::Types::ISO8601DateTime, null: false, description: '作成日' field :updated_at, GraphQL::Types::ISO8601DateTime, null: false, description: '更新日' def thumbnail_url object.image.url end end
この DSL なら GraphQL の知識さえあれば
title
は String 型で non-null でブログタイトルを表すthumbnail_url
は String 型で nullable でサムネイル画像 URL を表すcreated_at
は ISO 8601 フォーマットの DateTime 型で non-null で作成日を表す
のように、Ruby が読めなくともなんとなく理解できそうです。この部分の実装をモブプロ等で行うことで API の本実装開始前にスキーマの定義ができそうです。
スキーマ定義部分はそのままリゾルバとしても利用され、必要に応じて上記の例の #thumbnail_url
のように実装を追加すればそのまま API の実装が完了します。
おわりに
やってみた感想としては、スキーマが実装から生成できるので乖離がなく、スキーマ定義と実装が同時にできるので開発速度向上が狙えたのではないかなと思いました。
生成されるドキュメントによって仕様を伝えられたり、API スキーマの用語で議論できるようになったのでコミュニケーションコストが下がったのも良かった点の1つです。
今回は始めにスキーマを定義する方法をとりませんでしたが、今後開発を進めていってチームで課題を感じることがあれば試してみたいです。