開発者体験を爆上げするPlatform Engineeringへの挑戦 ー 汎用化のための設計編

こんにちは、ソーシャル経済メディア「NewsPicks」のプラットフォームエンジニアリングチームの崔(ちぇ)です。今年の上半期には、開発者の生産性を爆上げするため、システムの設計を根本的に見直すことにチャレンジしました。今回はそのお話ができればと思います。

もし「他社のプラットフォームエンジニアリングの事例が知りたい」「そもそもプラットフォームエンジニアリングチームは何をするチームなのか知りたい」「大きめの設計のフローが知りたい」など色々と気になっている方はぜひ読んでみてください!

ジュニアもシニアも同じ工数で安定的なシステムが作れる世界を目指す

現在の課題

NewsPicksは、アプリとWebの二つのプラットフォームから、お役に立ついろんな経済情報をお届けしています。記事、動画、本、コラムなどの多種多様なコンテンツをわかりやすくご提供できるよう、テーマに合わせて複数のタブやセクションに並べています。

FYI

  • フィード: 時系列に流れてくるデータの一覧
  • タブ: 異なる複数のセクションを表示するインタフェース要素で、下の画面の「ホーム」「トップ」「動画」などがそれに該当する
  • セクション: コンテンツをリストやカルーセルなどでまとめて表示したインタフェース要素で、タブの中にある「今日のニュース」「話題をまとめ読み」などがそれに該当する

NewsPicksのアプリ画面

NewsPicksのウェブ画面

これらのスクショをご覧になって気づいたかもしれませんが、複数のタブやセクションが並んでいても、なるべくデザインを揃えています。すると、似たもの同士は処理を抽象化して拡張しやすくしたいところです。ですが、実はそうなっておらずかなり複雑なシステムになっています。以下の画像のようにControllerからRepositoryまで、そのタブやセクション専用のものがたくさん実装されているのです。

一般的に、システムは流動的なものです。サービスの初期段階では、数年後が予想できず「これできっと大丈夫!」と思うでしょう(『The Rules of Programming』でも最初に一般化しすぎるなと述べていますし)。しかし、要件が一つ二つと増えていけば行くほどコードが複雑になるのです。例えば、直近のNewsPicksはより多くの価値がお届けできるよう、「本」タブ「コメントタイムライン」タブなど色々と出面を増やしてきました。期間限定で選挙タブなどを設ける場合もあります。その度それ専用のコードが増え、複雑さが増しました。

機能を新たに追加するために、シニアのエンジニアが少なくとも1スプリント(約一週間)かけるとしたら、軽い気持ちで「これ試してみるか」「新機能を追加してA/Bテストしてみるか」とはできなくなります。時には、スピードを重視しすぎるあまり、パフォーマンスの悪化に気付けずリリースする場合もあります。現実的な話として、「お掃除系タスク」ってユーザーに直接影響しないが故に、優先度が落ちがちです。

なので、あるタイミングには長期的な投資だと腹を括り変革を起こす必要があります。

前述した弊社が抱えていた問題をまとめると以下の二つです。

  1. お掃除する余裕のない開発体制
  2. 要件の積み重なりで開発しにくくなったシステム

これらの問題を解決すべく、以下のような変革を起こしました。

  1. お掃除する余裕のない開発体制 → 開発生産性・業務効率化を主担当とするプラットフォームエンジニアリングチームの発足
  2. 要件の積み重なりで開発しにくくなったシステム → 全体的なシステム設計のやり直し

これから、私がプラットフォームエンジニアリングチームのメンバーになり、最初に取り組んだ「全体的なシステム設計のやり直し」について詳細にお話ししていきます。

あるべき姿

まず、あるべき姿とは何かから考えました。実現可能性はともかく「ジュニアもシニアも同じ工数で安定的なシステムが作れる世界」を目指すべきだと思いました。

冒頭でお見せしたNewsPicksの画面を参考に、現在の構成を整理してみました。

  • タブは単なるセクションを束ねる箱
  • セクションは単なるコンテンツを束ねる箱

もちろん、コンテンツにもデザインにもいくつかの種類がありますが、逆にそこだけが差ということにもなります。なので、毎回、同じような設計と実装を繰り返す必要がないのです。箱の作り方を汎用化し、あとはコンテンツ取得ロジックを必要に応じて追加するだけにすれば、とても便利だと思いませんか!デザインはビューのお仕事なので、Enumで区別するだけにします。

理想から設計してみる

とってもシンプルな構成をご紹介しましたが、実際は諸々考慮すべき点が多く、このようにシンプルにはできませんでした。本番には例えば、下記のような様々な細かい要件がたくさん存在し、無視できないからです。

  • 一つのタブには必ずN個の広告セクションを表示しないといけない
  • 意図してはいないがXXというバグがあるので、それを考慮する必要がある
  • 現在のディレクトリの構成上、そこにはクラスが置けない
  • ユーザーのタイプや好みによって別のセクションでタブを構成すべきである
  • ...

ここからは理想像をより実現可能な形に具体化します。そのためには、なるべくシンプルに問題を定義した方がいいです。現状からかけ離れすぎないようにして、以下のような仮の状況を考えます。

  • タブは一つだけ存在し、タブ名は home である
  • home には3つの異なるデザインのセクションが存在する
    • セクション1は、速報「記事」を「リスト」で表示する
    • セクション2は、人気「動画」を「カルーセル」で表示する
    • セクション3は、人気「記事」を「ランキング」で表示する
  • セクションに表示すべきコンテンツは「記事」と「動画」の2種類のみである
    • 「記事」には「動画」が紐づかないケースがある
    • 「動画」には必ず「記事」が紐づく
    • コンテンツをPickするとPick数が表示される
  • N+1を許容しないので、必ずそのDBには一回だけリクエストする

前提の状況が設定できたので、GitHubに新たなリポジトリを作成しました。小さい問題に限定して新しい環境で技術検証をするのです。仮に、本番のリポジトリで試そうとすると、数時間後には新たな修正が入りコンフリクトするかもしれません。または、ローカル環境が壊れてやりたいことと関係のない問題に頭を悩まされるかもしれません。

上記の条件を満たす設計(最初から小さい問題に限定した設計)をしたら以下のようなものが出来上がりました。

汎用的なControllerはリクエストを受け付けると、「箱(セクションを含むタブ)を作るクラス」に home タブを作ってと呼び出します。そこからは下記の流れになります。

  1. タブの定義とセクションの定義をDBから取得する
  2. 表示したい内容別(速報記事、人気動画、記事の人気ランキングなど)に、表示対象のID覧を取得する専用モジュールが存在する。必要に応じて各モジュールにID一覧を取得してもらう。必ず各テーブルに一回だけリクエストする(DBへのアクセスを図では省略しています)
  3. タブ単位で表示すべきコンテンツID一覧を、重複を除いた形でコンテンツ取得クラスに渡す
  4. コンテンツタイプ別(記事、動画など)に、データを取得する専用モジュールが存在する。必要に応じで各モジュールにコンテンツデータを取得してもらう
  5. 各モジュールは、各テーブルに必ず一回だけアクセスし、データを取得する
  6. 箱を作るクラスは、返ってきたすべてのコンテンツデータを適切に箱に詰める
  7. 内容が詰まった箱を返す

前述のように、大事なのは誰でも開発がしやすい仕組みにすることです。そして、誰が開発しようとパフォーマンスのデグレが発生しない基盤にすることです。こういった観点で、この設計のポイントを以下にまとめます。

  • 開発者は、必要に応じて以下の対応だけをすればいい。つまり、新たにControllerやFacade、Service層のクラスを実装する必要がない
    • タブやセクションの定義をDBに追加する
    • セクションのデザイン(Enum)を追加する
    • モジュールを追加する
    • Repositoryにコレクション型対応のメソッドを生やす
  • いろんな種類のコンテンツが混ざっていても、必ずコンテンツタイプごとに1回だけDBにアクセスする
    • 例えば、通常の記事と他のコンテンツタイプ(動画など)に紐づいている記事をまとめる作業は、「コンテンツを取得するクラス」が行うので、開発者は気にしなくていい
    • 各モジュールのインタフェースはコレクション型でしか受け渡しができないようになっているため、N+1になることはない(初めて作る場合は、我々のリファレンス実装を参考にするはずで、それを見てもあえてfor loopを回すことはないと信じます)

この設計に従い、実装をしてみると本番にも組み込めそうということがわかりました。これで、我々の設計の方向性や実現可能性が確認できました!

終わりに

今回のような、システムの設計を根本的に考え直す作業は初めてでした。上長から「XXができる設計をしてみてください」とリクエストされ初めて設計したものは、全く汎用的な設計になっておらず(現実問題を考えすぎたり、実現方法が思いつかなかったりと)現在の本番に近いものでした。その後、上長やチームのメンバーにFBを受け、そこで得たヒントをもとに設計し直し、本番に組み込める状態にするまで2〜3ヶ月かかりました。その一連のプロセスの中で、「あるべき姿から考える」という意識が芽生えた気がします。

本記事はMockの設計をするところまでを記録しました。続編では、本番への組み込みを試みたこと、その際に工夫した点などをまとめたいと思います。続編も引き続きよろしくお願いします!

Page top