はじめに
こんにちは!ソフトウェアエンジニアの yamakazu (@yamarkz) です。
ネットスーパーを日常使いしていますが、スーパーに並ぶ商品の季節感が好きで週に1度は店舗に足を運びたくなってしまい、結局リアル店舗の利用は外せないという生活をしています。
さて、前回は開発文化の話を取り上げて紹介しましたが、今回はより技術寄りな話としてStailerのサーバーアプリケーションで採用しているDartパッケージを紹介します。
類似のトピックとしてアーキテクチャなどは石田さん (@wapa5pow) がリリース直後に紹介してくれていますが、Stailerで使われている具体的なパッケージなどはまだ紹介していませんでした。
StailerのサーバーはDartで書かれており、世間的にもあまり例がない技術選定がされています。(以下参考)
採用例が少ないDartという技術選択の中で得た知見をベースに、どういったパッケージを使っているのか?どういったパッケージがおすすめなのか?という問いに答えていこうと思います。
本記事を読んで、Dartでサーバーアプリケーションを開発することに少しでも興味を持っていただければ嬉しいです。😄
パブリックパッケージ
※ ⭐️ の数はパッケージの良さを個人的な推し度で表したもの
. gRPC with Protobuf (⭐️⭐️⭐️)
StailerのWeb APIではgRPCが採用されています。
Protocol Buffersでスキーマを定義し、クライアントとサーバーでインターフェースを共有して、リクエストとレスポンス処理を行うという一般的な使い方です。
10Xではタベリーの頃からgRPCを採用しており慣れていたのと、REST APIにしなければいけない制約もなかったので、引き続きgRPCを採用したと聞きました。
Dartの公式ではHTTP ServerにはShelfというパッケージが推奨されています。 個人的にはShelfを使ってみたい気持ちもありますが、gRPCの生産性やパフォーマンスの高さが優れている面を評価すると、実務ではgRPC優勢かな〜という気持ちです。
. retry (⭐️⭐️⭐️)
リトライ処理にはretryを採用しています。
Stailerは単体でもサービス立ち上げが可能ですが、一部パートナーのAPIを利用して機能を実現している箇所があり、そういったシーンで活用されています。
APIリクエストは大半が成功しますが、稀になんらかの予期せぬ理由で失敗することがあります。 イレギュラーなケースに遭遇してもリトライでカバーできる状態を作り、機能の完全性を担保することは運用で楽をする上で不可欠です。
httpの標準の機能でもあるので、それだけでよければそっちを使っていたりもします。
. clock (⭐️⭐️⭐️⭐️⭐️)
"時間"をシステムで扱うのは難しいですが、clockを使えば扱いやすくなります。
clockは現在時刻の概念を簡単に扱えるようにしてくれるパッケージで、活用するとテスタビリティが向上します。
test('error if delivery date has not been reached', () async { await withClock(Clock.fixed(DateTime(2021, 09, 28, 13, 30)), () async { // Written test code }); });
(withClockで特定の日時で処理を実行する例)
特定の時間順序に依存したテストを書きたい場合は、clockを用いて時間操作を容易にしておき、指定時間に処理を実行することを想定したテストコードを記述すると良いです。
主にdailyのバッチ処理系のテストコードで活躍してくれる欠かせない存在です。
. euc (⭐️⭐️⭐️)
Shift-JISのファイルを扱うためにeucを採用しています。
Stailerはパートナーから膨大な量のデータを連携しています。共有されるファイルにはShift-JISが採用されているケースもあり、Shift-JISも扱えるようにするためには文字コードの変換が必要で、そこで利用するのがこのパッケージです。
大抵のことはカバーしてくれるので重宝しているのですが、1つだけ惜しいところがstream処理過程でconvertする機能が未対応です。 ここは自前で列分割を行うパッチ処理を挟んで対応していたりします。
. sync (⭐️⭐️⭐️)
並行処理を行うために採用しています。
syncはGo言語にあるパッケージの思想をDartで表現したもので、インターフェースなどは同じです。StailerではこのsyncパッケージをラップしてConcurrentクラスという表現を作り、並行処理を行なっています。
syncによって1店舗あたり2~3万件ある商品データを350店舗分決まった時間内に同期することができるようになります。スケーリングの観点では絶対外せないパッケージです。
[余談] Null Safety化を進めるために沢田さん(@swdyh)がシュッとPRを送ってくれてたりします 🙏
. collection (⭐️⭐️⭐️⭐️)
複雑な配列操作を行う際にはcollectionを使うのがおすすめです。
firstWhereOrNull
や whereNotNull
でNullを上手く扱った処理を書いたり、 UnmodifiableListView
で配列操作に制約を加えたりもできて安全に便利な配列操作が実現できます。使わない選択肢がないくらい便利です。
. sentry (⭐️⭐️⭐️)
エラーログのトラッキングではSentryを使っています。
サーバーはログが膨大なので、Sentry経由で問題のログに早期に気づくことができた方が運用的には良いです。
Flutter 用には sentry_flutter があるようですが、StailerのClientでは採用していません。
. dartis (⭐️⭐️)
DartisはRedisを扱うDartクライアントで、シンプルなデータキャッシュとして使用しています。
主にパフォーマンス改善で活用しており、売り場で表示されるカテゴリーデータなどは更新頻度が低いためキャッシュを効かせてレスポンス速度を向上させています。
深い使い方をしていないのと、メンテナンス性が高くなさそうなので推しポイントは低いです。
. uuid (⭐️⭐️⭐️)
採用してます。RFC準拠で何も文句がありません。
. html (⭐️⭐️⭐️)
HTMLを楽に扱う時にはhtmlが良いです。
Stailerではパートナーが運営するネットスーパーのサイトをクローリングして商品データを集めるケースがあり、その場合にhtmlは活躍します。
趣味のクローラーを作ってちょっとした情報収集〜といったユースケースにも使えそうです。
プライベートパッケージ
Stailerで使っている自前のパッケージです。
. Firestore
Stailerの永続データストアにはCloud Firestoreが採用されています。
FirestoreのパッケージはPub.devでもいくつか確認できますが、そのほとんどがFlutter SDKを前提に作られており、Pure Dartではないです。
サーバーでも使えるかもしれませんが、使う中で下手に気を使うよりも、自分たちの前提にあったものを使うほうが中長期的に良いという考えから、自前でパッケージが作られました。
機能は公式で提示されているものを凡そカバーしており、最近だとSelect field機能が試験開発されています。
また、通信で利用されているデータのシリアライズとデシリアライズのパッケージをmirrorからjson_serializableに書き換えなども行なっており、パフォーマンスが大きく改善されたりしました。
. Firebase Auth
Firebase AuthもFirestoreの話と同じく、for Flutterなパッケージは存在しますが、サーバーで利用する(Pure Dart)ものは今のところありません。
サーバーを介して認証を行えるようにするために、Firebase Authのパッケージも自前で作られました。
プラットフォームとして扱える様にいくつかカスタマイズを加えていたりもします。
. BigQuery
BigQueryもDartのパッケージが存在しないため、自前で対応しています。 パッケージがないから採用を見送る... の意思決定はなかったです。(BigQuery最高!!)
StailerではBigQueryをフル活用して膨大な在庫データを扱っており、システム基盤としては欠かせない存在です。 (データの話は奥が深く、また別で取り上げられると思うのでお楽しみに!)
売り場に表示される商品データは、Raw Data → Storage → dbt → BigQuery → Firestoreという変遷を辿って実現しており、BigQueryのパッケージはこの中の BigQuery -> Firestoreのステップで主に使われています。
変わった使い方はしておらず、googleapisにあるBigQuery APIをラップして作られています。
. 決済処理系
Stailerは決済で外部ベンダーのAPIを利用しており、扱う処理はパッケージに切り出されています。
決済系は公式がライブラリ提供してくれているところもありそうですが、基本はWeb APIのみの提供になるので、それをDartの世界で扱いやすくするためにパッケージにしています。
. パートナー連携系
パートナー連携は連携ごとにAPI操作を行うパッケージを作っています。(決済処理系と同様)
機能をサーバーの各処理(API or Batch or Worker)で使い回すのと、"パートナー連携"という文脈で処理をまとめた方がわかりやすいです。
連携系のAPIはそれ自体はアトミックな操作にしておき、サービスクラスを挟んで各々の文脈で使いやすい形に変えたり、エラーハンドリングを加えたりしています。
言葉だけだと伝えづらいのですが、API Request → Service → Partner Client → Partner APIという流れの中でPartner APIを呼び出しやすくするためにClientをパッケージに切り出すということです。
まとめ
- パブリックパッケージを程よく取り入れるが、基本は薄く使う。
- プライベートパッケージは主にミドルウェア方面で自作している。
- 必要であればコントリビュートやパッチを当てて使う。
最後に
Stailerのサーバー開発で採用されているパッケージを紹介してきました。 どんな雰囲気で開発されているのか、少しイメージが膨らんだのではないでしょうか?
端的に言えば、パブリックパッケージを薄く採用しながら、ミドルウェア周りのパッケージを自作し、必要であれば適宜パッチを加えるという感じです。
Dartはまだまだマイナーですが、言語機能自体は素朴で読みやすいのでおすすめです。Server Dartも可能性は0ではないので、ぜひトライしてみてください!🎯
宣伝
10Xではソフトウェアエンジニアを募集しています。本記事を読んで少しでも気になった方はぜひ声をかけてください。Dartを駆使して一緒に小売のデジタル化を進めましょう!🔥
定期的にオープンオフィスを開催しています。会社のことから仕事のこと、パートナーとの関わり方など、ざっくばらんにメンバーが話をしているのでぜひご参加ください。