こんにちは。スマートシティ事業本部の大村と、同じくスマートシティ事業本部の野々村です。
本記事では、このたびリリースされた、今すぐ入れる近くの飲食店を予約できるサービス「
Neee
」を支えるバッグエンド技術とフロントエンド技術について、それぞれ大村と野々村から紹介させていただきます。
今すぐ入れる!近くの飲食店を予約できるサービス「Neee」とは
「Neee」は、近くにある今すぐ入れる飲食店を探しているお客様と、当日の空席を埋めたい飲食店をリアルタイムにマッチングするサービスです。外出先での急な飲み会の予定や二次会に行くことが決まった時に、近くにある今すぐ入れる飲食店を探すことは難しく、直接店舗に行くか電話で空席を確認しなければいけないといった課題がありました。 そこで、「Neee」では、来店されるお客様向けに現在予約受付中で近くにある飲食店のみをウェブサイトで紹介し、即座に予約ができるサービスを提供します。
本サービスは、DeNAのスマートシティ事業が掲げる「より便利な日常とワクワクする非日常を提供し、人々のQoLを向上させる新しい都市空間を創造する」というミッションのもと、横浜市の旧市庁舎街区活用事業のプロジェクトに代表する横浜市の関内エリアの「集客・再訪・回遊」を促進する取り組みの一つとして開発されました。
リリースは横浜市の関内エリア限定でのスタートになりますが、今後は提供エリアの拡大を目指します。外出先での飲食店検索・予約体験をアップデートし、街で発生している賑わいを周辺に広めるという世界を実現していきたいと考えています。
バックエンドアーキテクチャ
それでは、バックエンドで採用されているアーキテクチャから簡単に紹介します。本サービスは3つのプロダクトによって構成されています。
- ユーザ向けウェブアプリケーション (ユーザWeb)
- ユーザが現在位置から近くにある今すぐ入れる飲食店を検索し、予約することができます
- 店舗様向けウェブアプリケーション (店舗Web)
- 店舗様がNeeeに加盟店登録を行うためのものです
- 店舗様向けスマートフォンアプリ (店舗アプリ)
- 店舗様が予約受付ステータスを変更したり、予約の承認などを行うためのものです
これらのプロダクトを作る上で特定のクラウドベンダーである必要がなく、チームのエンジニアも3人と少人数だったため利用経験の多いGoogle Cloudを使っています。APIとウェブアプリケーションはCloud Run、RDBMSはCloud SQL、非同期処理にはCloud Tasksを使っています。その他にもCloud StorageやCloud Load Balancingなど様々なサービスを利用しています。これらの選定理由についていくつか紹介していこうと思います。
Cloud Runを採用した理由は以下のものが挙げられます。
- エンジニアが少ないのでフルマネージドがよかった
- サービスの特性として飲食店の空いていない時間はほとんどリクエストがなく、夜にリクエストが増える
1つ目の「エンジニアが少ないのでフルマネージドがよかった」についてです。本サービスに関わるエンジニアは3人でその中でも業務でクラウドサービスを扱ったことがあるのは1人だけだったため、必要な専門知識、かける工数を少なくしたかったためです。
2つ目の「サービスの特性として飲食店の空いていない時間はほとんどリクエストがなく、夜にリクエストが増える」についてです。サービスの性質上深夜から早朝にかけてはリクエストはほとんどなく、夜がリクエストのピークになると想定しています。(まだリリースしたタイミングなので実績がないため) 深夜や日中はポツポツとしかリクエストが来ないのでオートスケーリングはコスト最適化するために必須でした。
BFF(backend for frontend)の採用
BFFはアーキテクチャの設計パターンの1つです。クライアントとバックエンドの中間に位置し、双方の複雑性を吸収するために作られたものです。本サービスにおけるBFFの採用メリットは以下のものが挙げられます。
- フロントエンドに応じた認証に対応可能
- 複数のフロントエンドから同じAPIを使いたいケースではCore APIでビジネスロジックを共通化できる
- そのAPIで取得できるリソースを取得する権利があるかはBFFで判定する
- そのAPIで取得できるリソースを作成、更新する権利があるかはCore APIで判定する
- 今後別のフロントエンドが必要になった時に追加が容易
- Core APIはフロントエンドでどう使われるかを意識する必要がない
- BFFでフロントエンドに適した形に情報を加工することができる
- フロントエンドごとにメンテナンスを切り分けやすい
フロントエンドに応じた認証に対応可能
ユーザWeb、店舗アプリではどちらも
Firebase Authentication
を採用しています。
ユーザWebでは誰でもFirebaseユーザを作成し、サービスを利用することができます。一方店舗アプリは弊社で審査を行った店舗様のみ利用することができ、店舗アプリからFirebaseユーザを作成することはできません。フロントエンドに付与する権限が違うためユーザWebと店舗アプリではFirebaseプロジェクトを分けています。そのためバックエンドでFirebaseのIDトークンを検証するときもIDトークンのFirebaseプロジェクトに応じて検証する必要があります。BFFが分かれているためそれぞれのBFFは対応したFirebaseプロジェクトとしてIDトークンを検証すれば良くなります。
複数のフロントエンドから同じAPIを使いたいケースではCore APIでビジネスロジックを共通化できる
例えば任意の予約IDの予約情報を取得するAPIが必要な場合、ユーザWebであれば自分の予約でなければ取得できるないバリデーションが必要です。また店舗アプリでも自分の店舗の予約でなければ取得できるないバリデーションが必要です。BFFを作らずに1つのAPIサーバーでこれを実現しようとするとAPIを分けて作るか、IDトークンのFirebaseプロジェクトによる判定などが必要となります。
本サービスではCore APIではそういったバリデーションは行わず、シンプルに指定された予約IDの情報を返すAPIとし、BFFがCore APIから受け取った予約情報のユーザIDまたはストアIDで判定することができます。これによりビジネスロジックに変更が必要になった場合でも1箇所の変更で済みます。注意点としてはCore APIのレスポンスを見てバリデーションしているため、更新処理などはCore APIでバリデーションを行う必要があります。
今後別のフロントエンドが必要になった時に追加が容易
例えば今後管理者用のWebツールが必要になった場合でも新しくBFFを作成することでCore APIに手を入れることなく認証や権限管理が可能です。
Core APIはフロントエンドでどう使われるかを意識する必要がない
例えば本サービスでは各店舗の曜日ごとの予約受付時間をデータとして持っています。ユーザWebで必要なのは今日の予約受付時間だけで他の曜日のデータは入りません。そのため今日がどの曜日なのか判定する必要があります。このように時間を判定するような処理は原則フロントエンドではなくバックエンドで行います。
本サービスではCore APIは各曜日のデータをそのまま返し、BFFで今日の曜日を判定しレスポンスに今日の予約受付時間を返します。こうすることでCore APIはフロントエンドでの使われ方を意識する必要がなくなります。また任意の予約IDの予約情報とそれに紐づく店舗情報を取得したいという場合、通常は通信回数を減らすため両方を取得するエンドポイントを作ると思います。
BFFを作る場合、まず予約情報をCore APIから取得し、その情報のストアIDをもとに店舗情報をCore APIから取得し、合わせてフロントエンドに返すということもできます。こうすることでCore APIはシンプルなAPIを提供し、BFFが必要に応じて複数のAPIを組み合わせることができます。もちろん1つのAPIで予約情報と店舗情報が取れる方がフロントエンドから見たレイテンシは速くなりますがBFFとCore API間の通信はフロントエンドの通信環境に依存しないので数十ミリ秒程度しか変わりません。本サービスでそのくらいの違いは問題ないと判断しこちらの方法を採用しています。
フロントエンドごとにメンテナンスを切り分けやすい
こちらはそのままでユーザWebだけメンテナンスに入れたい場合、ユーザ向けのBFFでメンテナンス処理を行うことで店舗アプリはそのままでユーザWebだけメンテナンスに入れることができます。
Cloud Tasksを使った非同期処理
Google Cloudで非同期処理を実装する場合、 Cloud Tasks と Cloud Pub/Sub があります。本サービスではCloud Tasksを使っています。本サービスには予約が入ってから一定時間予約の承認が行われなかった場合、自動で予約をキャンセルするという仕様があります。この仕様の要件としてタスクの配信時間のスケジューリングが必要なためCloud Tasksを選択しました。
Amazon Connectを使った架電処理
本サービスではユーザから予約申請を行い、店舗様が店舗アプリで承認するという流れになります。私たちは承認されるまでの時間をできるだけ短くすることが重要だと考えています。ユーザから予約申請が入った時に店舗アプリにPush通知を送っています。しかしPush通知だけでは店員さんが常に端末のそばにいるわけではなく、店内が賑わっていたりしてPush通知に気づかないケースも多いです。そこで Amazon Connect を利用し電話をかけることで店員さんにすぐに予約に気づいてもらえるようにしています。
フロントエンドアーキテクチャ
次に、フロントエンドのアーキテクチャについてです。
ユーザWebと店舗WebはNext.js/React、店舗アプリはFlutterで作られています。
本記事では、Flutterで開発した店舗アプリのアーキテクチャについて簡単に紹介します。
Flutterの選定理由
開発当初、Flutterを選定した理由としては、以下のようなものがあげられます。
- 単一コードでクロスプラットフォーム対応が可能で、生産性が高い
- 近年Web対応も進み、将来的にWebアプリケーションとして提供することも視野に入れることが可能
- UIコンポーネントが非常に充実している
- Neeeのプロダクトの性質上、Platform Nativeな機能をほとんど必要としない
- HotReloadなどに代表される開発体験の良さ
開発時点では、Flutterでの開発経験があまり多くないメンバーがほとんどでしたが、 実際に採用してみたところ、上記であげたような効果を実際に感じることができ、非常に満足しています。
アーキテクチャ
次に、Neeeで採用したアーキテクチャについてです。
- レイヤードアーキテクチャを採用
- 単方向なデータフロー
- Application層のみがFlutterに依存し、Domain層とInfrastructure層はpureなDartコードのみに依存するように
-
Riverpod
を用いたDIとData Binding
- 基本的に、画面ごとにStateNotifierを継承したViewModelを用意し、Riverpodを用いてWidgetとDataBindingを行う
- Viewからの入力やイベントをもとにRepositoryからのデータの取得や更新を行い、自身のStateを更新することで、Viewに通知
- 基本的に、画面ごとにStateNotifierを継承したViewModelを用意し、Riverpodを用いてWidgetとDataBindingを行う
-
freezed
を用いたimmutableなstate/modelの管理
- 全ての状態はimmutableな形で管理し、意図しない副作用をなくす
- RepositoryのレイヤーでDataSourceを隠蔽
リリース時点の現段階では、基本的には全ての画面を上記アーキテクチャで実装することができており、 レイヤーを分離することで依存関係を統制し、複数人の開発でも品質を安定させる当初の狙いは達成できたと感じています。 また、各レイヤーの依存管理は全てRiverpodでDIされているため、非常にテスタビリティが高い開発を行うことができました。
一方で、画面ごとにStateNotifierを継承したViewModelを用意するという点に関しては、今後アップデートが必要だと感じています。 主な理由としては、
- ViewModelがfatになりがち
- Riverpodが提供するFutureProviderをもっと活用したい
といったところがあげられます。
現状、画面ごとにStateNotifierを継承したViewModelを用意することで、情報量の多い画面になるにつれて複数のRepositoryから複数の状態を取得し、それらを一つのstateで管理することとなり、ViewModelが肥大化してしまう傾向がありました。
今後に関しては、画面に対するViewModelを用意するという形に拘らず、情報の性質ごとにProviderを分離し、それぞれ独立してWidgetに提供するといった形を取り入れて行く予定です。
中でも、Repositoryから取得されるような非同期な状態の提供に関しては、FutureProviderを積極的に活用することで、上記の改善効果に加えて、
現状Repositoryの実装で独自に行っているデータソースのキャッシュ処理をFutureProviderのキャッシュ機構に委譲できるようになることと、
現状手続的に処理しているエラーハンドリングを宣言的に行うことができるようになる効果も期待しています。
Web対応に向けた考慮
店舗アプリは、現状iOS/Androidアプリのみの提供を行っていますが、今後Webアプリとして提供することも想定した実装を行っています。 そこで最後に、Web対応に向けて開発当初から実装面で考慮した点を簡単に紹介します。
- Navigator2.0の採用
- 画面ごとにユニークなURLを定義し、Webブラウザーの戻る・進むとロケーションバーの更新対応を行うには、Navigator2.0対応がほぼ必須
- 「宣言的にNavigationを実装できる」というのは聞こえがいいが、実際にそのまま利用しようと思うと実装がかなり煩雑になるため go_router パッケージを採用
- 一方で、Dialogなど遷移元画面に対してoverlayで表示される画面や、入力フォームに対する確認画面など一部の画面に関しては、URLだけでは画面の状態を表現するのが難しく、遷移前の状態をクライアント内で引き継ぐ必要があるため、既存の手続的なNavigatorを併用
- Globalなステートを極力利用しない
- Webに関しては、常に初期画面から順番に遷移されることを保証できず、アプリでは遷移前の画面などで生成されていたGlobalなステートが存在しないケースが発生するため
- Globalなステートはログイン状態などに限定し、画面のスコープを超えてクライアントで保持したい状態はキャッシュとして扱う
- 今後サーバーデータのキャッシュに関しては、FutureProviderを活用する予定
以上となります。
さいごに
今回は、このたびリリースされた、今すぐ入れる近くの飲食店を予約できるサービス「Neee」のバックエンドとフロントエンドの技術に関して、簡単に紹介させていただきました。
さいごにみなさまへお願いがあります。Neeeは現在横浜市関内エリアでのみご利用いただけます。横浜市関内エリアにお越しの際はぜひ Neee を使ってみてください!
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。