クックパッド開発者ブログスマートフォン用の表示で見るクックパッド開発者ブログ2017-04-06ユーザー基盤を作り直しながらRailsでのサービス層に向き合うこんにちは。わたしはいまクックパッドの「ユーザー基盤」を再構築しようとしています。一口に「ユーザー基盤の再構築」といっても、そのゴールが何を指すかは(わたし自身にとってもまだ)漠然としており、固定されたゴールは見いだせていません。後述するように、いくつかの問題は明確な形を取っています。言い換えると、それら明確な問題と向き合いながら『柔軟でいい感じのユーザー基盤を目指す』というのがこの再構築プロジェクトの目的です。その第一歩目として、ユーザー登録部分を現状のクックパッド本体とは別の小さなRailsアプリケーションとして実装を進め、つい先日、一部の限定された利用者の方に向けて公開することができました。今後も様子を見ながら公開範囲を拡大していく予定です。再構築の背景ではその「明確な問題」とはなんでしょうか。最大のものは、ユーザー登録のあり方が時勢にそぐわなくなってきているのではないかという懸念です。クックパッドのアカウントはメールアドレスとパスワードを登録していただくのが基本になっています。世の中のトレンドとしてEメール自体の利用シーンが減るに連れ、それを必須とするサービスまで使いづらいものになるのではないかという危機感がありました。そのため、ユーザー基盤に大きく手を入れたいというニーズがありました。一つは、クックパッドのコードベースがかなり巨大化・複雑化していることです(参考)。巨大で複雑なコードベースの上では、上記のユーザー登録のあり方の抜本的に変更することは難しいですし、日常の開発においてもある機能の修正が他の機能のバグのきっかけになったり、自動テストの実行時間も長くなったりするなどの問題がでています。そのため各機能を別のサービスとして実装した上で連携させるマイクロサービス化を進めています。その方向性を踏まえ、今回のユーザー基盤の再構築でも、ユーザー登録やログインといった機能を別サービスとして実装することにしました。サービス層への興味また、せっかく新しくRailsアプリケーションを作るのだから「Railsにおけるサービス層の実現」という技術的関心にも向き合うことにしました。Railsは、DBのテーブルをそのまま読み書きするような単純なアプリケーションを作ることは簡単にできますが、より複雑なアプリケーションではそのぶん慎重な設計が必要になるため、Trailblazerのような複雑なWebアプリケーション構築を支援するライブラリが注目を集めています。私自身もこのサービス層の実現にはおおいに興味があるいっぽう、今回はまだコード規模が小さいためにTrailblazerそれ自体の導入は先送りし、Rails上でフォームオブジェクトパターンによるサービス層の実現に挑戦することにしました。この記事では、そういったビジネス上の背景と技術的興味を踏まえてアプリケーションを作る過程の試行/思考を紹介します。最小限の機能から少しずつ進める再構築を決意したとは言え、サービスにおいて「ユーザー」というのはとても大事かつ複雑なドメインモデルです。実際に、現クックパッドのActiveRecordモデルクラス(以下ARモデル)Userはファイル中で定義されるメソッドが328個、includeしているモジュールが14個、has_(one|many)合わせて175個の関連が定義されている3,000行を超えるクラスです。このすべてを一気に新しいユーザー基盤(以下、新基盤)に置き換えようとしても、影響範囲が大きすぎてリリースにたどりつけないのではないかという懸念がありました。そこで今回は最小限の機能をリリースすることを重視し、従来より負担の少ないユーザー登録フローを実現するアプリケーションを開発しました。現方式では(1-1)ファーストビューで必要な情報をすべて入力したあとで、(1-2)到達確認のためにメールを送信し、(1-3)メール中のURLにアクセスすると登録が完了するという流れで登録します。対して新基盤では、(2-1)ファーストビューではメールアドレスのみ入力し、(2-2)すぐに到達確認メールを送信し、(2-3)メール中のURLにアクセスしたあとに残りの情報を入力する、という流れです。ファーストビューでの入力項目が減る分、ユーザー登録する際の負担が減るであろうと見込んでいます。新基盤のデータの持ち方やインフラ構造についても、現行クックパッドからの漸次的な改善を進めていくために、以下のような方針を取りました。新基盤では、現クックパッドとDBを共有する。新しいフローでのみ必要なデータは新規のDBに、現クックパッドで使うデータは現DBに書き出す。現DBに書く場合でも、ビジネスロジックは新たに書き直す。以下の特定のパスをリバースプロキシで分岐して新基盤と統合する。技術要素は社内推奨かつ新しいものを採用する。Railsを採用する。ECS上にアプリケーションをデプロイするためのHakoなど、社内の標準的なインフラ構成に乗せる。このように「最小限の機能で、ちゃんと動くソフトウェア」をリリースすることを優先した結果、ユーザー登録のみを行うアプリケーションとして最初のリリースを迎えられました。アプリケーションを少しずつ設計するさて、再構築の方針やファーストリリースのスコープは決めたものの、それだけではアプリケーションはできません。今後の拡張に対して柔軟に対応でき、かつ複雑だったり無理のある構造にしないため、慎重にアプリケーションを設計しました。その過程も紹介します。メールアドレスを登録する出来上がった新基盤でのユーザー登録フローでは、利用者が目にする最初の画面は下記のようなものになります。これをみてわかるように、入力フォームはメールアドレス一つだけというシンプルなものにしました。この画面からメールアドレスを登録すると、メールアドレス確認用のリンクが記載されるメールが送られます。利用者がそれをクリックすることで、メールアドレスの到達確認がなされたとみなします。そのため最初は、「メールアドレス登録」を表すARモデルを作り、メールアドレスと確認用URLに付与する予測が困難なトークン文字列(以下、確認URL用トークン)を作りました。カラム定義は次のようにしました。updated_at)このデータを永続化するとともに、入力されたメールアドレスに対し、確認用メールを送ります。確認URLから登録完了する確認用URLからアクセスする画面はこのようなものになります。ここでパスワードと生年月日を入力してもらい、メールマガジンなどの設定を見直してもらうと登録が完了します。一見すると単純な画面ではあるのですが、ここから登録すると下記のような、たくさんの処理が走ります。現DBのユーザーテーブルにデータを作る。合わせて、現DBのユーザーのデータ一式を作る。それらは複数のテーブルに分割されている。新基盤のDBでも、そのメールアドレス登録を使用済みとして更新する。登録完了のメールを送信する。他システムにユーザー登録完了を知らせるためのAPIを叩く。そこで、この処理ではARモデルを直接読み書きするのではなく、フォームオブジェクトを導入し、「ユーザー登録完了フォーム」として抽出しました。これにより、以下の観点で「無理のないアプリケーションコード」にできたと思っています。OldなRubyオブジェクトにできた。そのため、モデルのユニットテストとして一連の処理をテストできた。複数ARモデルのオブジェクトを一気に保存する場合に、関連先オブジェクトの内部構造と密結合してしまいやすいaccept_nested_attributesを避け、ネストのないActiveModelモデル内にカプセル化できた。これで主要な入力は出来てきましたが、前のメールアドレスの登録画面や、それを永続化するテーブル構造に気になるところがでてきました。前述のように、メールアドレス登録フォームでは、入力されたメールアドレスと確認URL用トークン、有効期限を1つのテーブルに永続化していました。これは初手としては手頃な落とし所ではありましたが、いくつか気になる点がでてきました。再構築の目的には、近い将来にメールアドレス以外でのユーザー登録できるようにしたいというものもありました。すると「利用者がユーザー登録しようとしたこと」そのものと「そこで登録してくれたメールアドレス」は分割すべきように見えます。登録完了した時点で現DBに書き込まれるユーザーも追跡可能にしておきたくなります。それらを考慮し、テーブル構造を以下のようにしました。updated_at)なお「現DBのユーザーID」と「登録完了日」をのみを抽出して「登録完了したこと」テーブルに分けるアイディアもあり、個人的にもそのほうが好みではありました。そこを分割する利点がまだ少ないこと、将来的にそうしたくなった場合でも無損失に分割できそうなことなどを考慮して、いまの2テーブルの構成にしています。このようにテーブルを分割するとRailsからも複数のARモデルを扱うことになります。結果として、メールアドレス登録画面にもフォームオブジェクトを導入することでスッキリ書けるようになりました。まとめこのように、機能がまだ少ない簡単なWebアプリケーションであっても、アプリケーション設計で考えられることはたくさんありました。今後も継続的に開発を進めながら設計を洗練させていきたいと思っていますし、あるいはTrailblazerの導入なども考えていくつもりです。これ以外にも、Reactを使ったクライアントサイド設計や、その前提となるサーバ側/JS側での責務の切り分け、自動テスト戦略などなど、あらためて言語化してみると様々なレベルで設計判断が必要となりました。別の機会に、それぞれ紹介できればいいなと思います。今回の「最小機能でリリースする」というスコープ決定や、今後もやりたいことリストから取り組む順番づけなど、ソフトウェア開発は大小の判断の連続なのだなあというのをあらためて実感します。いった判断は、それぞれの現場によって、やりたいことも前提も既存コードも大きく違うため一概には言えないと思いますが、今回の話が一つの例として参考になれば嬉しく思います。