every Tech Blog

株式会社エブリーのTech Blogです。

ネットスーパーアプリ GraphQL から REST へ移行始めました

はじめに

こんにちは、retail HUBで Software Engineer をしているほんだです。
今回は私が現在着手している事業譲渡されたアプリを社内で持続的なプロダクト開発を行える状態にするリプレイスプロジェクトをどのように行っているか紹介しようと思います。
この記事ではリプレイスを行うにあたってどのようなことを課題に感じてその課題に対してどのような解決策をとったか主にサーバーの実装について説明しています。

ネットスーパーアプリとは

現在弊社ではネットスーパーアプリとして Web アプリとスマホアプリの二つのシステムを提供しています。
Web アプリは販促コンテンツの設定や売り上げの管理・集計を行うことが可能な管理システムと受け取り方法に応じた価格変更や送料変更にも対応し、消費者の柔軟な買い物を実現するお客様向けアプリを 17 の小売り様に、スマホアプリでは Web アプリのお客様向けアプリと同等の機能を Android と iOS のアプリとして株式会社リウボウストア様にリウボウネットスーパーとして提供しています。
こちらのサービスは以前株式会社ベクトルワン様が開発・運用していたものを事業譲渡されたものです。

リプレイス前の実装

リプレイス前の実装は上記図のようになっていました。
ネットスーパーアプリは GraphQL Mesh で作成された GraphQL Gateway Server を呼びその裏では AppSync と Lambda を用いて GraphQL が実装されていました。
GraphQL のリゾルバーに当たる Lambda は Python で書かれていました。

リプレイスの背景

課題点

既存の実装では下記のような問題があったため今回リプレイスを行うに至りました。

  • 社内に知見が少ないインフラ構成や、言語で実装されている。
  • Appsync, Lambda を用いた GraphQL の実装がチューニング不足もあるかもしれないが遅かった。
  • 重複する場合やコアとなるロジックを切り出すのに Lambda レイヤーにする必要があり管理が大変だった。

リプレイスを進めるにあたり満たしたいこと

リプレイスを進めるにあたり満たしたいこととしては下記のようなことを意識しています。

  1. このプロダクトは現状は1小売様向けとなっていますが今後小売りの拡大やバグを見つけたときに早期対応、機能の追加をできるような持続的なプロダクト開発をできるようにする。
  2. DB は既存の Web アプリのものを用いるため大量の table 、小売りごとに特定の table の有無があるものを適切扱えるようにする。
  3. サーバーの実装と同時並行でアプリの実装もすすめられるようにする。

リプレイス後の技術スタック

リプレイス後のインフラ構成は上記図のようになる予定です。
リプレイス前に用いていた GraphQL Gateway Server は GraphQL を REST に移行また今後は REST に統一していく点から導入している必要がなくなったため廃止しました。
満たしたいこと 1 にあげた持続的なプロダクト開発をできるようにすることを満たすためになるべく社内に知見があるものを選定するようにしました。
インフラに関しては社内の他のサービスでも使われていて知見が豊富な ECS を、開発に用いる言語に関しても Python から社内の知見が豊富な Go、framework は echo を採用しました。
満たしたいこと 2 DB を適切に扱えるようにすることを満たすために Go の ORM は sqlboiler を採用しました。具体的な理由については後述します。
満たしたいこと 3 サーバーとアプリの実装の最適化を満たすために OpenAPI を用いたスキーマ駆動開発を実践しています。OpenAPI を用いてエンドポイントの仕様を事前に決めておくことでサーバーとクライアントが並列に実装を行えるようにしています。
OpenAPI 定義書の作成には Stoplight Studio を用いています。
次にリプレイスにあたり特筆する点について説明していきます。

oapi-codegen

oapi-codegen は Stoplight Studio で作成した OpenAPI 定義書から Go のコードを作成するために用いています。
oapi-codegen を用いて Go のコードを作成することで Request Header の値や Query Parameter の validation を自分で実装する必要がなくなります。
また、ルーティングも任せることは可能ですがその場合全てのエンドポイントに middleware を反映することになり個別に設定することができなくなってしまうため今回は用いていません。
main 関数の実装は下記のようになります。

func main() {
    e := echo.New()

  wrapper := openapi.ServerInterfaceWrapper{
        Handler: handler.NewHandler(chainSchemaMap),
    }

  g := e.Group("")
    g.Use(echomiddleware.Recover())

  // 認可なしエンドポイント
  {
    g.GET("/policy", wrapper.GetPolicy)
  }

  // 要認可エンドポイント
  g.Use(middleware.Authorize())

  {
        g.GET("/items", wrapper.GetItems)
        g.GET("/items/:id", wrapper.GetItem)
    }

  e.Logger.Fatal(e.Start(":1323"))
}

sqlboiler

sqlboiler は toml ファイルを記述し実際に DB に接続することでその table 定義を元に Go の struct を生成することができ、既存の table の数だけ stuct として書き直す手間が省けます。
今回、小売ごとの DB の差分は特定の table の有無なため local 環境に全ての table を持つ DB を作成し、それを参照することで小売ごとのカスタマイズを含む全ての table の struct を作成できます。
Go のサーバーと DB の接続には一つのユーザーを用いているため、どの小売のアプリがどの DB にアクセスできるかは Go で map を定義することで対応しています。

Stoplight Studio

スキーマ駆動開発のための OpenAPI 定義書は Stoplight Studio を用いて記述しています。
Stoplight Studio は GUI 形式で OpenAPI 定義書を編集できるツールとなっています。
また、ツール内から Mock Server や実際のサーバーを叩くことができるので OpenAPI を書く用途だけでなく、どのようなリクエストでどのようなレスポンスが返ってくるかも確認することも容易となっています。

まとめ

まだリプレイス作業が始まったばかりでレイテンシーの改善などは具体的に測れていないの結果として捉えることは今後やっていく必要があるなと感じました。
リプレイスを行っていくにあたっても最終系から逆算し何が必要かということをまとめられていなかった点もあり適切な工数見積もりができなかったり後手になることもあったので今後はそういった点も意識していきたいです。
今回私自身実務の Python のコードに触れるのが初めてでそれを慣れ親しんだ Go に書き換えるという経験は言語の長所などを改めて捉え直す貴重な機会になったなと思います。