Business Managerの開発で生じた課題

中村俊之氏:では、そろそろ本題のBusiness Managerの開発で生じた課題と、それに我々がどう対応したか、という学びをいくつか共有していきたいと思います。

まずは他のシステムとデータ連携していくという話です。ここで何度か出てきているシステム概要図をご覧ください。Business Managerでは、DBにある権限設定の変更をLINE DMPに、Kafkaを通じて「変更された」ことを通知しています。

ここには、いくつか課題がありました。そのうち1つが、データベースとKafkaを同じトランザクションでどうハンドリングするか、という問題です。

Business Managerでは、管理画面からユーザーが共有権限の設定変更をできます。その際にLINE DMPに確実に漏れなく、かつ、正しい順序でデータが変更されたということを通知する必要がありました。

なぜかというと、LINE DMP側では、例えば「オーディエンスデータの共有が開始された」または「共有が止まった」というBusiness Manager側での変更を、Kafkaを通じて正しい順序で受信することで処理しているからです。

ここで問題になったのが、データベースとKafkaを同じトランザクション内でどうハンドリングするか、ということでした。実現する方法をいくつかチームで議論しましたが、3つほど案が出ました。

1つ目の方法は、まずAPIコールバックでデータベースの同一トランザクション内でDB更新処理をした後に「共有権限が変更された」というメッセージのパブリッシュを行います。その後、コンシューマー側でAPIを呼び出して、変更がデータベースに反映されているか、ということを確認してもらう方式です。

トランザクション内でKafkaにパブリッシュされたものの、ロールバックされてデータベースにデータが反映されていないケースが起こり得るので、API呼び出しが必要となっています。

この方法のメリットとしては、何よりも実装がシンプルである点、また、他のチームでもよく使われている実績のある方法という点がメリットでした。

我々は初期開発で時間がなかったため、これらのメリットは非常に魅力的ではありました。しかしながら、コールバック用のAPIにパフォーマンス上での懸念があったため、この方法は早い段階で見送ることにしました。

2つ目は、変更データキャプチャ(CDC)を使った方法です。我々の部署ではMySQLのbinlogから、Debeziumというソフトウェアを使って変更をKafkaに通知する方式を使っている実績がありました。

この方法のメリットとしては、ほぼリアルタイムで処理できること、また、他のチームですでに利用されているという点でも、メリットがありました。

しかしながら、共有権限の設定を管理しているテーブルの構成が、複雑で複数にまたがっていることから、コンシューマーの設計が難しそうでした。またインフラのセットアップなど、別途作業が発生するのもスケジュール的に厳しそう、というデメリットがありました。

また、他のチームで採用されている方法だと、順序保証がされないため、結局結果を確認するためのAPIコールバックが必要になるので、この方法も見送ることになりました。

3つ目の方法としては、共有権限設定が変更された際に、権限設定の変更だけではなく、イベントのキューに当たるテーブルにも同一トランザクションでイベントを書き込む方式です。その後、イベントキューのテーブルをバッチでポーリングして、変更があればKafkaに通知する、という方法です。

送信が終わったことは、レコードが送信済みであることを意味するカラムをアップデートすることで、2度Kafkaに送信されないようにできます。

この方法のメリットしては、実装がシンプルなことと、順序保証もされることでした。デメリットとしては、ポーリングの間隔次第では、リアルタイム性が失われること、また、変更が多い場合にはスケールしないということが挙げられます。

しかしながら、Business Managerの要件としてはリアルタイム性はそこまで必要ないという点、また、頻繁に権限設定を変更するようなユースケースはないだろう、ということで、この選択肢が採用されました。

最終的なアーキテクチャ

最終的なアーキテクチャは、いくつかの改良を加えてこのようなかたちになりました。共有権限、設定変更とイベントキューに当たるテーブルに、同トランザクションでデータを書き込みます。

1つ目のバッチでイベントキューのテーブルから変更を読み取って、共有権限設定を利用できるかたちに解決して、別DBのテーブルに書き込みます。今、「利用できるかたちに解決された権限設定」と言いましたが、Business Managerの機能解説で少し触れたように、Organization内のすべてのアカウント間で全種類のデータを共有する、といったワイルドカードのような設定ができる仕様があるので、このデータ解決が必要になっています。そして2つ目のバッチで、解決された共有権限設定をKafkaに通知する構成になっています。

2段構えに分かれているメリットとしては、トラブルが起きた際に、Kafkaへ送信ステータスを管理しているカラムを変更するだけで再送信でき、リカバリーのオペレーションがしやすいといった理由もあります。

複数のチームで連携しながらの開発

システム連携の次の話題は、複数のチームで連携しながらの開発についての話です。また少しだけシステム概要の図に戻ります。私は、主にBusiness Managerのバックエンド開発を担当していますが、Business Managerのバックエンド開発チームでは、いくつかのREST APIを他のチーム向けに提供しています。

まず管理画面用のAPI。これは管理画面がシングルページアプリケーションで作成されているため、フロントエンドチームが開発に利用しています。

そして次に、内部のシステム向けAPIがあります。LINE DMP向けには共有権限設定を返すAPIが存在していて、LINE公式アカウントやLINE広告にはBusiness Managerにアカウントを接続するためのAPIが存在しています。

Business Managerの初期の開発では、私が所属しているバックエンドチームが、これらのAPI仕様を決める必要がありました。スケジュール上も、クリティカルパスになってしまうので、他のチームになるべく早めに提供する必要がありました。

ここで課題になったのは、APIドキュメントを作成・メンテナンスするコストです。社内でも部門により違いますが、これまで私が所属している部署では、社内のWikiにAPIドキュメントを記載する方法を採ってきました。この方法だと、仕様が変わった場合に、コードとドキュメントを二重でメンテナンスするというコストが発生します。

springdoc-openapiの採用

Business Managerの初期の開発においては、難しい仕様を理解しながらスケジュールが厳しい中、APIドキュメントの作成を迅速に行う必要がありました。この問題に対して我々は、springdoc-openapiを採用することで対応しました。springdoc-openapiは、Springのコントローラーにアノテーションを付けるだけで、OpenAPIバージョン3形式のJSONやYAMLを生成してくれるライブラリです。

特定のエンドポイントでSwagger UIを利用可能になるので、コントローラーから生成された最新APIドキュメントをいつでも閲覧できるようになります。そして閲覧するだけではなくて、実際にAPIを実行してレスポンスを確認するといったこともできます。

Kotlinサポートもされていて、Kotlinの型がNon-Nullであれば、必要な要素だとみなしてくれたりと、非常に相性も良くなっています。そして、プロジェクト独自のアノテーションを読み取ってドキュメントに必要な項目を追加するといった、プログラムを書いてカスタマイズするといったことも可能です。

それではもう少し具体的な利用方法をお見せします。Springのコントローラー作成時に「@Tag」や「@Operation」や「@Schema」など、Swagger APIのアノテーションを付けます。

するとこれだけで、Swagger UI上でAPIドキュメントが見られます。Swagger UI上からAPIの実行も可能です。

このようなOpenAPIバージョン3形式のファイルを、特定のエンドポイントで取得できるので、Swagger UIで見る以外の活用も可能です。例えば、我々はGitHubエンタープライズを使った開発をしていますが、Pull-Requestを送った際に、ブランチごとにAPIドキュメントを生成する工夫もしています。これでレビュアーの人がAPIの仕様を確認しやすくなっています。

なお、静的ファイルを社内のファイルストレージサーバーに上げる方式の相性の関係で、この際のビューワーはReDocを利用しています。

また、Business Managerの管理画面を作成しているフロントエンドチームでは、TypeScriptを使ってシングルページアプリケーションで画面を開発していますが、openapi-typescriptというライブラリを利用することで、TypeScriptの型を生成してバックエンドのAPIとの統合が効率化されます。

openapi-typescriptのコマンドを打って、APIのパスであったり、リクエストやレスポンスのTypeScriptの型定義を生成できます。これによってAPIが追加されたりとか、変更があった際にもスムーズに対応できます。

最後に少し応用的な方法についても説明します。Business Managerの管理画面においては、企業のユーザーの利用を想定しているため、「admin」や「一般ユーザー」といったロールごとに権限管理を行う機能があります。

我々はAPIエンドポイントで、このロールのユーザーがアクセス可能といったことを、コードは例になりますが、独自のアノテーションを使ってコントローラーに入れて制御しています。

springdoc-openapiが提供しているオペレーションカスタマイサーというインターフェイスを実装したクラスで、@Authという独自のアノテーションを読み取るようにして、springdoc-openapiの初期化時に登録するようにします。

すると、どの権限がこのエンドポイントにアクセス可能かを自動的にドキュメントに反映させられます。このスライドにあるように、左のコードで設定したMarkdown形式の文字列が、右にあるSwagger UI上のドキュメントに反映されます。

プログラミング手法の話

次いで、プログラミング手法の話についても触れておきます。Business Managerでは、とにかくIDの種類が多いという問題がありました。Business Managerでは、他のアカウントと接続して、そのアカウントのデータを共有するというシステムの性質上、とにかくIDの種類が多いです。システムで扱うIDは、データベースのPKのIDも含めると20種類以上になります。

例を挙げると、Business ManagerのOrganizationのIDやLINE広告やLINE公式アカウントのID、オーディエンスのデータやLINE TagのID、そして管理画面にログインするユーザーのIDなんかもあります。そして今後も機能追加によって、接続するアカウントやアカウントに紐づくデータの種類はさらに増えていきます。

これらのIDをプログラミングで、間違えずに取り扱えるようにするために、我々はIDをValue Objectとして扱うように工夫しています。

Value Objectとは、Domain-Driven Designの文脈で登場する「値が同一であると比較できること」そして「イミュータブルであること」を特徴とするオブジェクトのことを指します。Kotlinにおいては、データクラスでプリミティブの値をラップするだけで、簡単に作成できます。Kotlinのコードの例ですが、「FooID」というデータクラスが1行で表現できています。

Value ObjectのIDをコントローラーで受け取った時から、データベースにインサートやアップデートするまで、プログラム内で一貫して利用するようにしています。このことで、プログラミング上、代入のミスなどが起きないことを保証しています。実際のコード例を記載していますが、Value Objectで書くと、引数の代入ミスなどはコンパイル時にエラーになるので、起こり得ません。

また、Value Object化したことによる他のメリットもあります。先ほど紹介したspringdoc-openapiの仕組みと組み合わせることによって、ドキュメンテーション作成も効率化できます。定義したIDにアノテーションを付けて、ドキュメント上でのIDの定義も使い回せるようにしています。

さらに今回のBusiness Manager開発においては、Kotlinの機能を有効活用していることも触れておきたいです。Kotlinの基本的な機能にはなりますが、Null safetyによってNullableなクラスとNon-Nullを区別して安全にプログラミングできています。

また、Business Managerでは、アカウントやデータなどTypeが多いですが、今後も増えていくことが予定されています。「Sealed class」や「Sealed interfaces」「Enum」あと「when」で分岐を網羅できる機能を利用して、アカウントやデータのTypeごとにうまいこと処理をハンドリングしています。

さらに開発当初はエクスペリメンタルだったため、まだ利用はしていませんが、パフォーマンス上のオーバーヘッドなしでプリミティブな値をラップする「inline class」も活用していきたいなと考えています。

以上のように、Kotlinの開発においては、Kotlinは開発において便利な機能がたくさんあり、私の所属している部門では、Kotlinがかなり活用されるようになってきています。今回のLINE DEVELOPER DAY 2021でもKotlinについてのセッションもあるので、興味ある方はぜひ見てみてください。

モブプログラミングで開発

さて、ここまでいくつか技術的な内容を説明してきました。最後にもう1つ、Business Managerの開発で学んだ内容を共有します。

Business Managerの開発は、1月に発足した新チームでの開発でした。また、コロナ禍だったため完全にフルリモートでの開発でした。そんな中でチーム開発を開始しましたが、いくつか効果的だったと思うプラクティスがありました。

まず、新チームで開発するうえでの細かい認識を合わせたいと考えて、モブプログラミングを試しました。モブプログラミングとは、チーム全員でプログラミングをすること。言ってしまえば、ペアプログラミングを2人より多い人数ですること、というとわかりやすいかもしれません。

ドライバーと呼ばれる人が、実際のコーディングをして、ナビゲーターと呼ばれる人が、その他の人がどうコードを書くかを指示していきます。

通常は、画面やPCやキーボードを1台だけ共有して作業することが多いようですが、我々はフルリモートで開発しているので、Zoomでドライバー役、つまり実際にコーディングしている人の画面を共有するかたちにしました。

ドライバーが交代する際には、Gitのブランチに変更をプッシュして、次のドライバー役の人が自分のマシンにブランチをプルして開発するようにしています。

また、プログラミングだけじゃなくて、リリース作業やインフラ関連の作業なども、Zoomで画面を共有してモブで作業するようにしています。

これは個人的な感想ですが、チームで技術的な知識を共有したり、新しいチームでチームワークを作りたいみたいな時だったりに効果的なんじゃないかと感じました。

また、Zoomをずっとつなぎっぱなしにすることもやっています。Zoomを特定のルームで、カメラをオフ、マイクもオフの状態でつなぎっぱなしにすることで、必要な時にいつでも話しかけられるようにしています。

基本的には、Slackでの非同期コミュニケーションでの開発をしていますが、Business Managerの場合、仕様面で不明なことがあったりした場合に、画面を共有しながら話したほうが早い場合が多かったので、そういった場合に、とても役立ちました。

朝会とslack

次に、いわゆる朝会を毎朝午前11時から行うようにしています。チームで認識を合わせたい課題だったり、どんなささいなことでもいいので、とにかく話し合うようにしています。

また、タスク管理のためにその日やることをTrelloのタスクを取っていく形式にしています。少し変わったこととしては、TrelloとモニタリングツールのGrafanaを連携させて進捗管理をしたりもしています。

具体的には、ローカルPCからPrometheusのPushgatewayという機能を通じて、Trelloのポイントを定期的に集計して進捗状況を可視化する、ということをしていました。

本当はTrelloでBurndownチャートを使いたかったんですが、プラグインが有料だったり、カスタマイズが必要だったため、こんなかたちにしてみました。

最後に、リモート環境で働いていると、他の人の動きが見づらいと感じることが多いため、すべての通知をSlackに流すようにしています。

例えばGitHubでのプルリクエストの内容や、Trelloの変更、CIの結果など、またシステムリリースをする際も専用のBotがいて、Slack上から行えるようにしています。ただし、Slackで流れる頻度が多いものについては、チャンネルを分けるなどの工夫をしています。

まとめ

ではそろそろまとめに入っていきたいと思います。Business Managerは、マーケティングデータを活用するための次世代の基盤です。今後も接続サービスを拡大しながら、継続的に開発を続けていきます。ビジネス要件も複雑で、システム的にも解決するべき課題は多いですが、その分やりがいも多いかなと思っています。

もし今回の発表をお聞きになってご興味がある方がいれば、ぜひチームにジョインしていただければすごくうれしいなと思っています。以上で私の発表は終わりにいたします。ご清聴ありがとうございました。