最近はすっかりFlutterエンジニアになってます。id:kikuchy です。
Diverseが提供するサービスの一つに、youbrideという婚活サービスがあります。
この6月に、youbrideはAndroidアプリのデザインリニューアルを行いました。
変わったのは見た目だけではありません。
新アプリには、マルチプラットフォームフレームワークのFlutterを採用しています。
近々、iOSアプリもFlutterで開発したバージョンに置き換える計画が進行中です。
そして、APIサーバーもRuby on Railsを使ったものに置き換えています。
サーバーとクライアント間の接続にはgRPCを使用しています。
なぜyoubrideでFlutter / Ruby on Rails / gRPCを採用することになったのか?
それは、youbrideに発生していた種々の問題を解決するためでした。
youbrideを取り巻いていた問題
仕様の複雑化とコードの複雑化
youbrideは今年で運営19年目を迎える、長寿サービスの一つです。
私達、今のチームが運営を手がけるより前にも、複数の会社を渡り歩き、複数のチームが開発・運営に携わっていました。
歴史あるサービス故に、サービス方針などで迷走した時期もあったのでしょう。
複雑な仕様や把握が難しい仕様が存在し、それを表現するコードも複雑になっていました。
複雑故に依存関係も読みきれず、不要かどうかわからないコードが放置されデッドコードになっている箇所もあります。
エンジニアの人数が少ない問題
現在、youbride担当チームは以下のメンバーで構成されています。
- エンジニア …(サーバー/クライアント合わせて、外部委託の方も合わせて)5人
- デザイナー … 1人
- PO兼PM … 1人
膨大な仕様とコードに対して、対応できる人員が少なすぎます。
そんな状況だったので、一つの機能改修をするための影響範囲の調査だけで半日〜数日かかることもあり得る状況でした。
求人
youbrideのサーバーアプリケーションのコードはPerlで書かれていました。
Perlでの開発・運用をしたいエンジニアさんの数は少なく、採用の現場ではPerlエンジニアの募集がとても難しい、という声も上がっていました。
問題解決のために
コードの削減
少人数での開発を可能にするには、保守する対象のコードを削減する必要があります。
特にクライアントアプリケーションについては、マルチプラットフォーム開発ができる仕組みを使うことで、UIに関するコードやビジネスロジックをiOS/Androidの間で一本化できます。
また、ツールによる補助などを使用して、省力化できる部分は省力化したいところです。
仕様の単純化
利用者の皆様に御迷惑をかけてしまうため、サービス仕様については急に大きな変更をすることはできません。
せめてプログラム的な内部仕様は単純化したいものです。
そこで、サーバー - クライアント間の通信仕様の管理を簡単にできるものを探すことになりました。
人気の言語の採用
求人の対象は主に日本国内で、特にサーバーサイドで人気が高く、応募者が集まりやすい言語を選定することになりました。
技術選定
上記の要件に合致するものとして、最終的に以下のものを選択しました。
クライアント
Flutterを採用しました。
決定当時に普及の兆しが見え始めていたこと、
ロジックだけでなくUIも共通のコードで記述できること、
エコシステムが強力だったこと、
いざとなればプラットフォームの機能を簡単に使用できることなどが主要因です。
PWAにすることも検討されていましたが、iOS SafariではまだPush APIを使用できないことから、不採用となりました。
サーバー
Ruby + Ruby on Railsを採用しました。
国内の求人数が多いこと、
モダンな言語であり人気があること、
開発メンバーが慣れていることなどが主要因です。
JVM言語(KotlinやJavaなど)の使用も検討されていましたが、
メンバーのJVMサーバーのチューニングなどの運用経験が皆無であることなどから見送りとなりました。
インターフェイス
サーバ - クライアント間のインターフェイスにはgRPCを使用することになりました。
サーバーとクライアントの間での共通認識が作りやすいこと、
バイナリベースなプロトコルで、HTTP 2.0を使用できて高速に通信できること、
通信部分の実装をほとんど自動生成できることが主な要因です。
Swagger (OpenAPI)も俎上に上がりましたが、通信速度などの点でgRPCに軍配が上がりました。
実際に採用してみて
クライアント
途中経過の話は、以前にQiitaに投稿したことがありますので、合わせてご覧ください。
Developer Experience
FlutterはとてもDeveloper Experienceの良いフレームワークだと感じています。
アプリを実行したままコードの変更を反映させるHot Reloadがとにかく高速で、
これを経験してしまうと、通常のAndroidアプリのビルド時間にも耐えられなくなってしまうほどです。
また、(React風の)宣言的UI構築はとても直感的で、Viewの現在の状態を考える必要がなく、楽にコードを書くことができました。
高速な動作、使い勝手
Androidのアプリケーションとしてリリースを行いました。
FlutterデフォルトのLook & FeelがMaterial Designであるということもあり、通常のAndroidアプリケーションと使い勝手は何ら遜色ありません。
ビルド後のサイズも、今まで公開していたAndroidクライアントと全く変わらないサイズ(約15MB)になっています。
FlutterアプリケーションはDartのエンジンなどを積んでいるため、サイズは大きくなることを覚悟していましたが、軽く済んだのはとても嬉しい誤算でした。
サーバー
再設計による疎結合化、単純化
Perlで書かれていたサーバはいわゆるFat Controllerな設計になっており、適切なレイヤー化ができていませんでした。
今回、サーバーのコードもリライトする際にレイヤーを見直すことで疎結合な実装を実現でき、テスト駆動開発ができるようになりました。
同時に、Perlでは多用されていたメタプログラミングを廃し、IDEによる補助を強力に受けられるようになりました。
ちょっとした動作確認が簡単
Ruby on Railsの採用で、rails c
コマンドによる対話的環境で、簡単な動作確認が手元で行えるようになりました。
テストを書くほどではなく、少し値の形式を確認したいだけ…など、といったときに、心理的なハードルを低くして動作させることができるようになっています。
これまではサーバーを立ち上げて、試したいコードパスを通るリクエストを投げなければいけませんでした。
インターフェイス
gRPCは通信にHTTP/2を、データフォーマットにProtocol Buffersを使用したRemote Procedure Callの仕組みです。
共通のインターフェース記述言語(IDL)を用意し、そこからサーバー、クライアント用の実装を自動生成します。
インターフェースの曖昧さ解消
これまで、サーバー - クライアント間ではREST(っぽい)APIを定義し、HTTP/1を介してJSONフォーマットでデータをやり取りしていました。
また、Perlの値を安直にJSONへシリアライズすると、真偽値が"1"
/"0"
と、数値も文字列型として出力されてしまいます。
以前のクライアントアプリケーションはこれをなんとなく変換して使っていたのですが、
以前のメンテナがいなくなってしまった今は、果たしてこの値が真偽値として扱うことを期待されているのか、そうでないのか、判別できません。
gRPCには共通のIDLに型宣言でき、Enumも定義できることから、値域について混乱することがなくなりました。
実装の簡略化
gRPCはIDLからサーバー/クライアント向けのコードを生成できるため、具体的な通信にかかるコードを記述する手間をほとんど無くすことができました。
IDLの.proto
ファイルは専用のリポジトリで管理しています。
このリポジトリでPull Requestが通ると、CIでコードの生成を行い、サーバーとクライアントのリポジトリに自動的に生成したファイルを含んだPull Requestを生成するようにしました。
ドキュメントもGithubリポジトリのWikiに自動でPushされるようにしています。
ファイルの生成について特別気にすることがないため、とても開発が楽になりました。
課題
抱えていた問題のだいたいを解決することができましたが、新しい要素の採用故に生じた問題もあります。
クライアントの課題
言語
Flutter SDKはDart言語で記述されており、Hot Reloadなどの仕組みもDart言語を前提に作られています。
Flutterが出た当初はDartに対する不満も多く見られましたが、バージョン2.3になってだいぶ改善してきたように思います。
しかし現状ではまだ大規模開発を行う上で不満は残ります。
特に現在のDartはnull安全ではなく、予期しない入力によってnullを期待しないメソッド引数にnullが入ってしまった場合などに簡単に例外が生じます。
人間はミスをしがちな生き物であり、人間の注意だけでこうしたミスを防ぐのは困難なので、できれば型システムでnullが入る可能性があるのか否かを明示してほしいと切に感じます。
null安全性については今後のDart言語のバージョンアップでの採用が決まっているため、
プロダクションで採用するならば、個人的にはnull安全が導入された後のタイミングをおすすめします。
しかし悪い言語だということではありません。
言語仕様レベルでの非同期処理のサポート、クラスとインターフェイスの同時宣言、Collection If/Forなどコードを書きやすい機能も揃っています。
クラッシュレポート
現在はFirebase Crashlyticsにクラッシュレポートの蓄積を行っています。
(現在、Firebase CrashlyticsはFlutterを公式にはサポートしていません)
レポートの送信はfirebase_crashlyticsプラグインを用いて行っていますが、例外によってはDartのスタックトレースが送信されていないケースがままあります。
(スタックトレースを得られない原因は目下調査中)
また、スタックトレースが得られても、Widgetのレンダリングプロセスの途中で起きた例外は、Flutter SDKのクラス名が並ぶばかりで、ユーザーコードのどこで例外が起きたのかわからないこともあります。
そのため、現在は例外の原因究明に大きなコストがかかっています。
Firebase CrashlyticsがFlutterに正式に対応してくれるのを待っています。
gaurunとfirebase_messagingの相性
youbrideはgaurunを用いてPushサーバーを運用しています。
AndroidアプリケーションがリモートからのPush通知を受けるにはFirebase Cloud Messagingを使う必要があり、firebase_messagingプラグインはFlutterでのPush受信のデファクトスタンダードになっています。
ところが、firebase_messagingはFirebaseのコンソールなどから送信された形式のペイロードを想定した作りになっているため、gaurunが送信するペイロードとは形式が合わず、gaurunからPush通知を送ってもクライアント側で表示されません。
(gaurun側の修正予定もない、とのこと)
結局、プラグインに頼らずに、Push通知の受信を実装することになりました。
サーバーの課題
gRPCの運用経験
弊社内でgRPCを使用したプロダクトはyoubrideが初めてです。
そのため、大規模サービスでのgRPCの運用経験がありません。
インフラ構成なども手探りで進めているので、今後なにかトラブルがあったら怖いのは間違いないです。
ただ、大事故に至らないように調査はしていますし、今後知見をためていくことができれば不安は小さくなるでしょう。
インターフェースの課題
サーバーがCPUを使い切らない?
現在はgrpc gemを使用していますが、どうやらgrpc gemにはCPUを使い切ってくれない問題があるようです。
今のところはまだこれが問題にはなっていませんが、そのうちに問題になる時が来るかもしれません。
youbrideチームは、iOSアプリもFlutterで作ったものに置き換えるべく作業を続けています。
これだけ歴史の長い大きなサービスでFlutterを採用しているケースは、国内初なのではないかと思います。
今後もDiverseはコミュニティに知見を還元してゆきます\\ ٩( 'ω' )و //