マネジメント支援のSaaSプロダクト「Wistant」のバックエンド技術スタックを紹介します
こんにちは、RELATIONS奥宮です。
今年も残りあとわずか。平成最後の年末、いかがお過ごしですか? 自分は先日のクリスマス、人生で初めて妻と娘(ゼロ歳)と一緒に過ごしました。お風呂やら離乳食やら夜泣きやら、普段と変わらない1日でしたけどねー… ( ´ ▽ ` )
前回の久原さんのフロントエンド技術スタックの話に引き続き、今回は自分がバックエンドの実装に関して、マネジメント支援ツール「Wistant」の事例を引きながらお話したいと思います。
「Wistant」について
技術スタックに入る前に、Wistantがどういうプロダクトなのか、簡単に触れておきたいと思います。(プロダクトの特性が、技術やアーキテクチャの選定にも関わっていますので、前段として共有させてください)
Wistantは、従業員が数百名規模の企業をメインターゲットに、ピープルマネジメントの運用改善や仕組み化を支援するサービスです。
具体的には、現在以下のような機能を提供しています(HPより抜粋)。
目標管理
- メンバーが追っている目標を管理することができます。目標に対して振り返りをし、1on1を実施することでメンバーの達成を支援することができます。
1on1の運用・改善
- 1on1のペア作成、アジェンダ設定、分析、など1on1の運用や改善を楽にする機能が揃っています。
パフォーマンス分析
- 1on1後にメンバーは簡単な振り返りを行います。この結果から、組織のマネジメント状態を分析することができ、改善のための示唆が得られます。
これらのメイン機能以外に、一般的なSaaSに標準で必要な機能、あるいはBtoBツールならではの必須機能もあわせて実装しています。主だったのものは以下のとおりです。
アカウント管理
- 入退会、メンバーの招待、ロール管理、プロフィール変更、などの機能
課金機能
- サブスクリプション型の従量課金に対応した課金機能
閲覧権限制御
- 「Aさんの目標は<本人と直属の上長>のみ閲覧可能」のように、データソースごとに権限を設定できる機能
外部連携
- Slack等のチャットツールと連携し、通知をポストする機能
こうした一連の機能を、エンジニア総勢4名(「フロント:バックエンド」=「2:2」くらいの比率で稼働)で開発しています。
使用しているライブラリなど
構成については後述するとして、まずはバックエンドで使用している言語・フレームワーク・ライブラリについて、主だったところをざっとご紹介します。
Node
少人数の開発チームでいかに学習コストを抑えて開発効率をあげられるかを検討した結果、数年前から全社的にNodeを採用しています。
その狙いどおり、フロントとバックエンドの垣根なくJavaScriptに特化して開発を進められることで、相乗効果が生まれ、チーム全体としてノウハウやスキルレベルが向上したと思います。
技術的側面でいうと、Wistantではロジック層にReactive(RxJS)を採用したのですが、それができたのもNodeの特性(ノンブロッキング I/O、イベントループ)によるところが大きいです。
Koa
Webフレームワークについては、開発をはじめた2017年初頭時点で、Koa v2を選択しました。ExpressではなくKoaにした最大の理由は、async / awaitで非同期処理を書きたかったからです。
新規プロダクトなので下位互換などのしがらみ(笑)もなかったため、デファクトのExpressではなくモダンさを優先してKoaを選択した形です。後述するApollo ServerがKoa v2に対応していたのも大きかったですね。
Apollo Server (GraphQL)
APIには、RESTではなくGraphQLを採用しています。これも2017年初頭には意思決定しているので、プロダクトでの採用事例としては早い部類ではないかと思います。
Nodeで動くGraphQLサーバ、いまでこそPrismaとか便利なライブラリがありますが、当時はGraphcoolくらいしかなく(のはず)、自前サーバでGraphQLのバックエンドを動かすとしたらApollo Serverを立ててSchemaやResolverをゴリゴリ書いていくのみ…な感じでした。現在も、その実装をブラッシュアップしつつ引き継いでいます。
情報やツールの少ない中、導入にはもろもろ苦労がありましたが、ひとたび勘所を押さえてしまえばGraphQLは本当に便利!
とくにResolverの仕組みは秀逸で、複数のデータソース(DB, 外部 API, etc)を柔軟に取り扱えますし、type(データ型)に対してResolverを設定することでフィールド毎のアクセス制御も容易に実装できます。この利便性は、ちょっともう手放せない感じです。
GraphQL Subscriptionに関しても、Apollo Clientが正式対応した時点で導入しました。上記のメリットをソケットでもありがたく享受させていただいてます^^
Sequelize
ORMにはSequelizeを採用しています(DBはMySQL)。当初はAssociationなどもがっつり書いていたのですが、開発を進めるうちにORMのAssociationとGraphQLのResolverとの食い合わせが悪いことがわかり、Sequelize側からはAssociationなど外し、ごく薄く使う形に仕様変更しました。
現在は基本、マッピングとトランザクションとマイグレーションのみSequelizeが受け持っています。(それでも、DB スキーマとGraphQLスキーマの両方を書かなければいけないとか、まだイケてない面が残っていたり。Prisma…)
RxJS
「CQRS」なアーキテクチャ(後述)でロジック層を実装する目的で、RxJSを採用しました。
実装的には、GraphQLのMutationをトリガーにしてObservableを生成し、そのストリームに対して、認証認可・データ更新・各種通知(Email、Slack、WebPush、etc)などの処理をカスタムオペレータでつないでいく形で、ビジネスロジックを形成しています。
メリットとして、各カスタムオペレータごとに職責が分離しているので、コードの見通しがとてもよくなりますし、ロジック変更にも柔軟に対応が可能です。
コードを実際に見ていただいたほうがイメージ伝わると思いますので、下にサンプルコードを掲載します。
function entityCreating(source$, appContext) { const { log,Sequelize} = appContext; return ( source$ // イベントをフィルタ .typeOf('entityCreating') .log(log, 'info', '[entityCreating]', 'start') // リソースのロード .resource(util.loadResource(sequelize)) // 認可 .verify(spec.whenCreate, e => { const { args, actor, context: { resource } } = e; return [args, actor, { sequelize, resource }]; }) // データ生成 .execute(async e => { const { args, actor } = e; const entity = await service.create(args, actor, appContext); return { entityId: entity.id }; }) // タイムラインに feed を流す .feed(util.feedActivity(FeedTypes.created.value)) // web socket で publish .sideEffect('created', util.effected(appContext)) .log(log, 'info', '[entityCreating]', 'end') .share() ); }
その他
- ユーザ認証にはJWTを利用しています。
- GraphQLのフェッチ層のキャッシュにはData Loaderを使用しています。
- バッチ処理系は、LambdaのScheduled Eventで処理しています。
- 各種SaaSも利用させていただいています(クレジットカード決済はStripe、チャットボットはDialogflow、など)。
アーキテクチャ
CQRS ( with RxJS )
Wistantは企業単位/チーム単位で利用されるプロダクトという特性上、1回の処理に対して複数の通知を投げなければならないユースケースが多くあります。
たとえば、チームのAさんが目標の登録を行なった場合に、以下のような通知を行うケースを想定します。
- Aさんにメールで通知する
- チームリーダーにWebPushで通知する
- チームのSlackチャネルにフィードを流す
- チームの(Wistant上の)タイムラインにフィードを流す
こちらに対応するビジネスロジックは、たとえば以下のようなダイアグラムで表すことができます。
各タスクの中には非同期にできるものもあり、すべてのタスクが直列処理されるわけではありません。しかしながら、「目標を登録する」というユースケースに対するロジックとして一括りにするには、粒度が大きすぎる印象があります。
実際、当初Wistantはこのようなスタイル(Resolverにゴリゴリ処理を書いていく)で実装を進めていたのですが、複雑になっていくビジネスロジックに対応しているうちにコードの見通しがどんどん悪くなり、密結合でメンテナンス性も悪くなり...。
こうした状況を打開すべく、開発スタートから半年ほどのタイミングで「CQRS *1 」をベースとしたアーキテクチャに全面改修しました。
先ほどの例を、RxJSを利用してCQRS的に実装すると、以下のようなダイアグラムになります。
改修ポイントは以下のとおり。
- 書き込み系の処理と読み込み系の処理を明確に分ける(通知は読み込み系に分類)*2
- 書き込みに関しても、目標の作成とフィードの作成は別のユースケースと考えストリームを分離し、イベントをdispatchすることで呼び出す
- 各通知処理は、目標作成のストリームからイベントを受けとり、それぞれ個別に実行される(並行処理)
改修前と比較すると、タスクごとの職責が明確になり、フローの見通しが格段に良くなったかと思います。また、(これはCQRSというよりRxJSの功績になりますが、)各タスクが疎結合になったことで、コードのメンテナビリティもぐっと向上しました。
Wistantのバックエンドのビジネスロジックは現在、全般にわたって上記のようなCQRSベースのアーキテクチャを採用しています。
では次に、このアーキテクチャをAPIのインターフェイスとどう接続するかを見てみましょう。
with GraphQL
GraphQL(APIインターフェイス)も含めた、バックエンド全体の構成は以下のようになります。
CQRSパターンは、ズバリ、GraphQLとも相性がとても良いです。 (そもそもGraphQLのMutation/Queryというフレーム自体、CQRSをベースにしているのかも。詳しい方いらっしゃったら是非教えてください…!!)
例えば、Queryに関して。 CQRSの利点のひとつに、Command(書き込み)とQuery(読み込み)の職責を分離してすることで「読み取り側ではクエリ用に最適化されたスキーマを使用し、書き込み側では更新用に最適化されたスキーマを使用できる *3 」ことがあげられます。
その点、GraphQLは、定義されたスキーマの範囲であればクライアント側が自由にクエリを書いてデータを取得することができるので(あるエンティティの必要なフィールドだけ、とか、複数エンティティを1クエリで、とか、自由に指定できる)、CQRSのQueryの実装として完璧にマッチします。
これは、書き込み後にレスポンスデータを返すケース(Mutation)も同様です。Command処理後、イベントとしてルートエンティティ情報をQueryに渡すだけで、あとはGraphQLがよろしくクエリをさばいてくれます。クエリ内容に関して、Command側が意識する必要は全くありません。
このように、GraphQLに「CQRS+Reactive」なロジック層を組み合わせることで、それぞれの特性が存分に発揮され、柔軟でメンテナンス性の高いバックエンド構成が実現できたかな、と思っています。
まとめ
以上、駆け足になりましたが、Wistantのバックエンドの技術スタックの概要をご紹介しました。
他の多くの言語だとフルスタックなフレームワークがデファクトで存在しますが、Nodeでは薄いWebフレームワークにお好みでミドルウェアを追加していくのが一般的です。そのぶん、ロジック層にプロダクトごと・実装者ごとのカラーが出やすいかなと思います。
Wistantの場合、ビジネスの業務フローに沿ったサービスを提供する上で、取り扱うドメインは多岐にわたりますし、ユーザロールごとの権限管理など要件も複雑になります。
必然、バックエンドのロジックも容易に肥大化する傾向にあります。したがって、CQRSのようなフレームやReactiveのような宣言的なコードスタイルを導入して、やや重厚ではありますがメンテナビリティを重視した構成を採用しています。
これがもし、もっと軽量なサービスのバックエンドでしたら、GraphQLを採用するにしてもロジックはResolverに素直に書くだけで足りるかもしれません。あるいは、いまならAppSyncなどのサーバレスバックエンドの利用を検討するかもしれません。
ことほど左様に、最適な手段は相手や目的に依るもの。「技術やフレームワークは適材適所で」というのが、しごく当たり前ではありますが自分が技術選定する上で指針としているところです。
今回はWistantの事例をご紹介しましたが、また機会がありましたら別の事例についても掘り下げてお伝えできればと思います。そしてそれらが、みなさんの開発のご参考になれば幸いです^^
(...なお、インフラ構成に関しては時間切れ(納期に間に合わなかった...)につき、後日追記にて簡単に補足させていただければと思っていますmm)
それではみなさん、よいお年を! 来年も、RELATIONSならびに本ブログを、どうぞよろしくお願いいたしますー。
現在RELATIONSでは開発メンバーを募集中です!興味がある方は、まずはお話をできればと思います! https://www.wantedly.com/companies/relations
*1:CQRSについて詳しく知りたい方は、「CQRS アーキテクチャのスタイル 」や「 CQRSとイベントソーシングの使用法、または「CRUDに何か問題でも?」 」あたりが参考になるかと思います。
*2:通知に関しては、CQRS的なQueryとは厳密にはいえないと思いますが、この例では構造を単純化するため「読み込み」側に含めて考えます。(寛大なお心で何卒ご理解ください...mm)