技術的負債を徹底的に解消した話 - オミカレのシステムフル刷新のためにやったことを全部教える
技術的負債、デザイン面での課題など、サービスを構成するシステムを全面にわたってリニューアルしたプロセスを、オミカレの高橋一騎さんが克明に伝えます。
株式会社オミカレでテックリードをしております、高橋一騎(たかはし・いっき/ @ikkitang )です。私たちが提供する婚活メディアサービス「オミカレ」は、2019年3月にシステムのフルリニューアルに踏み切りました。本稿では、このリニューアルのプロセスをできるだけ詳細にお伝えしたいと思います。
さて、「技術的負債」という言葉を耳にすることがあります。なぜ負債が生まれるのか。「品質を犠牲にしてでも早々にサービスをリリースし、短期的にビジネスの速度を上げる」という判断はその理由の一つに挙げられるでしょう。エンドユーザーへの価値提供スピードを得るための見返りとして、あえて技術的品質を犠牲にする、という判断です。オミカレもまさに同じ判断により多くの技術的負債が生まれてきました。その結果、想定外の開発工数の増加、思わぬバグの発生といったネガティブな現象が頻発してきたのです。
業績をもっと伸ばしたいが技術的負債により開発速度が出ない。こうしたジレンマにぶつかったオミカレは新しい価値をエンドユーザーに早く届ける事を目標に、システムフルリニューアルを決断しました。
我々がいかにフルリニューアルを決断し、そして大きなプロジェクトをチームで乗り越えリリースにこぎつけたのか、その経験を余すことなくお伝えします。
- 旧システムの課題
- なぜ、私たちはフルリニューアルを選んだのか
- 課題を解決するための新アーキテクチャ
- 見積もり困難な巨大プロジェクトをチームで遂行するためにやったこと
- リリース後の継続的改善を担保する「改善Day」
- リニューアルによって得られたもの
- まとめ
旧システムの課題
オミカレとは
まずは簡単にオミカレのサービスについて説明をします。オミカレは、婚活パーティーを主催するイベント事業者の方に開催情報を掲載していただき、会員の方が掲載情報を見て、好みのパーティに参加申し込みをする、というメディアサービスであり、そのポータルサイトがオミカレです。
2015年7月に会員機能のサービスをローンチの後、11ヶ月で会員数が3万人を突破し、2020年03月時点では45万人以上のユーザーの方にご利用頂いています。
サービス要件の進化
上記のリリースは、あくまで「会員サービスのローンチ」であり、オミカレ自体のサービスローンチはさらにさかのぼります。現在では婚活パーティーへの参加申し込みは会員登録が前提となっていますが、以前はゲスト会員でも参加申し込みが可能だったのです。
会員機能のリリース後も契約プランの増加などがあり、サービス要件は大きく膨れあがっていきます。また今でこそオミカレのエンジニアは6名体制ですが、当時は社内のエンジニアは1名で、足りないリソースは外注のエンジニアによって補われていました。
こうした経緯から、スピード優先のリリースが実施されることが多く、売上のアップと引き換えにコードのメンテナンス性は確実に犠牲になっていきました。技術的負債の発生要因としては「よくある」事例でしょう。
また、犠牲となった品質はプログラミングコードだけではありません。弊社では当時専属のデザイナーやマークアップエンジニアがおらず、その場その場でデザインとマークアップエンジニアを外注し開発が進められていました。そのため、プロダクトデザインとしても一貫性が保たれておらず、スマートフォン利用が大半であるにもかかわらず、スマートフォン向けのデザインになってなかったり、情報がどこにあるか分かりにくい設計になってしまっていました。
以下では弊社のサービスで見られた負債の実例を紹介します。
グロースを妨げる技術的負債の例
検索しにくいデザイン
旧デザインはこのような感じでした。オミカレのコーポレートカラーともいえるピンクを基調にしていますが、「公共の場でピンクを基調としたサイトを開くのはちょっと恥ずかしい」といった意見を男性ユーザーからいただいていました。
また、我々が大きな課題ととらえていたものに、検索性の悪さが挙げられます。旧デザインでは「週末開催のパーティ」や「人気のパーティから選ぶ」といったように、ユーザーからすると受動的にパーティーを選ぶようなつくりになっていました。オミカレは「日本最大級のパーティー掲載数」と謳っていながら、実際にはユーザーが本当に自分の行きたいパーティーを見つけにくい、という大きな課題があったのです。
また、会員専用ページでも、スマートフォンで見ると「おすすめパーティー」が縦に並び、会員が自分の予約履歴を見るのにスクロールを繰り返さないといけない、という問題もありました。その他にもスマートフォンユーザーの利用が想定れていないデザインのページも多くあったのです。
遅すぎるページ表示
もちろん、デザインだけではなく技術面でも複数の課題がありました。
例えば、ページ表示の遅さです。Dockerで動かしてるlocalhost上の各ページの描画には5~6秒もの時間が必要でした。本番環境では多少のスピードアップはありましたが、それでも表示までかなり時間がかかっていました。ユーザー体験の面でもSEOの面でも、表示速度の遅さは我々が抱えていた大きな課題でした。根本解決のためには、バックエンド、フロントエンド、DB設計など広範にわたる改修が求められる課題です。
なぜ、表示に多くの時間がかかるのか。バックエンドを例にすると、不必要なDBアクセスが非常に多い、といった要因が挙げられます。当時のシステムでは、例えば、メディアTopページに各都道府県のパーティー掲載件数を表示していますが、そのページでは常に47都道府県の各パーティー掲載件数のSUMクエリが流れていたのです。しかも、WHEREに「満席のパーティは除外」や「中止となったパーティーは除外」などの複数条件が付いたうえで、です。当然フルスキャンでクエリは実行され、これが遅くなる原因になっていたのです。
フロントエンドに目を向けると、圧縮されてないリソースが読み込まれていたり、jQueryやBootstrapなどのモジュールが使用されていて、軽量化にも限界がありました。また、PHPのView内にJavaScriptがそのまま書かれていたり、style.cssというグローバルのCSSが存在し、わずかなデザイン調整作業が全ページに影響する、という事態も頻発していたのです。
深すぎるifのネスト
継ぎ足しを繰り返して機能追加されたことで生まれた最も大きな課題は、if文の複雑化とそれに伴うControllerのメソッド肥大化です。
以前は会員登録しなくてもパーティーの参加予約ができましたが、この機能を司るコードは以下のようになっていました。これは一例なので、省略や改変などを含みます。
public function reserve_example() { /* 補足コメント: sessionキーにuser_idがあれば取得 */ if ($this->session->has_key('user_id')) { $user_id = $this->session->get('user_id'); } else { $user_id = 0; } if ($this->db->get(['user.id' => $user_id])) { $user = $this->db->get(['id' => $user_id], 'user_table'); } else { $user = null; } $company = $this->db->get(['id' => $company_id], 'company_table'); $this->validation->set_rules(~~); /* 契約区分に応じてバリデーションを変更する */ if ($company['keiyaku_type'] == 1) { $this->validation->set_rules(~~); } else if ($company['keiyaku_type'] == 2) { $this->validation->set_rules(~~); } if ($this->validation->run()) { /* Validation成功時のコードを書く */ /* 契約区分に応じて登録する情報を変更する */ if ($company['keiyaku_type'] == 1) { $this->db->insert([~~~], 'reserve_table'); } else if ($company['keiyaku_type'] == 2) { $this->db->insert([~~~], 'reserve_table'); } /* その他、登録処理 */ } else { /* 例外処理 */ } }
上記のコードはほんの一例ですが、多いものではコード量が700行を超えるメソッドも存在しました。それもControllerのメソッドだけで、です。Modelのメソッドとして外に書き出されているものもあり、正常な動作系を見るのが非常に難しく、改修によって大小さまざまなバグが発生した時期もありました。
また、複雑になってたのはControllerのコードだけではありません。以下の例のように、Viewも非常に複雑になっていました。
<?php <div> <h1><?= $party['title'] ?></h1> <?php if($credit_party && $user['credit']): ?> <div> クレジットカードの登録は必須です <a href='credit/form'>登録フォーム</a> </div> <?php else: ?> <form> <div><input type="text" name="name"></div> <div><input type="text" name="tel"></div> <?php if($credit_party && $credit_type == 2): ?> <div><input type="radio" name="credit_type" value=1>クレカ決済</div> <div><input type="radio" name="credit_type" value=2>現金決済</div> <?php else if ($credit_party && $credit_type == 1): ?> <div><input type="radio" name="credit_type" value=1>クレカ決済</div> <?php endif;> ... </form> <?php endif;> </div>
ユーザーの契約状況に応じて、適切なformや情報を出す必要があり、Viewにif文が入ることが多く、改修箇所の特定が難しい、という問題がありました。
レジェンドコードという考え方
このように、さまざまな面で旧システムは技術的負債を抱えていましたが、その背景にある、「スピード優先で事業を進める」という判断が間違っていたとは思いません。たとえ、よろしくないコードであったとしても、それがオミカレのサービスとビジネスの土台を作ってきたことは確かですし、当初1名しかいなかったエンジニアから6名のチームとなるまでグロースを導いてきたコードなのですから。私は、以下の資料に示された「レジェンドコード」という考え方が非常に好きです。
技術的負債の存在は事実ですが、負債を生み出したコードによってサービスが支えられてきたのもまた事実です。失敗から学び、次に生かせばいいのです。次章からはオミカレが過去からなにを学び、技術的負債を解消するべく新たなアーキテクチャを作り出したかを説明します。
なぜ、私たちはフルリニューアルを選んだのか
さて、リニューアル実施前、我々が抱えていたのは、デザイン、プロダクトコードに加えて、検索順位が落ちてきている、という課題でした。
変化の早い商環境を生き抜いていくためには、強い基盤を素早く作り、サービスの新たな価値を素早くユーザーに届ける必要があります。そのため、私たちが選んだのは、デザイン、フロントエンド、バックエンド、インフラの全てをいちから作り直す、フルリニューアルという手段でした。
もちろん、デザインリニューアルとリファクタリングを別々に実施する、という選択肢もありました。ですが、デザイン変更のみを行おうにも、複雑なコードに阻まれスムーズにタスクを進めるのは難しく、リファクタリングだけを行おうにも、結果的にフロントは作り直しになるので、再度フロントは書き直しが必要になってきます。であれば、すべてを一緒にリニューアルしてしまおう、と決断しました。
なお、私たちは「フルリニューアルによって、すぐに業績が上がる」と考えていたわけではありません。今後、業績を上げるための投資としてフルリニューアルという選択をとったのです。
課題を解決するための新アーキテクチャ
では、課題解決のために私たちが選んだアーキテクチャとは。まずは旧システムを図示すると以下のようになります。
新たなシステムは以下のようなアーキテクチャを採用しています。
リニューアル内容について、いくつかを具体的にご説明しましょう。 まずはサーバーサイドでは、DBへの読み書きをAPI経由で実施する、つまりCodeIgniter上からDBに直接アクセスできないように構成を変更したのです。{$annotation_1}
ただし、この構成を実現するためには、開発環境ではWeb(PHP)とAPI(Python)の2つの環境が必要になります。こうした開発環境の複雑化に手軽に対応するべく、インフラでは開発環境を完全Docker化し、本番環境ではAWS Fargateを使用したコンテナ化を行いました。
Fargateの採用によって、オートスケールなどの対応がTerraformで完全コード化され、コンソールを使用する必要がなくなります。このことから、属人化の解消とサイトの可用性向上にも寄与するのです。また、Amazon ECRによるイメージ管理を導入したしたことで、リリースしてバグが起こった際のロールバックも非常に楽になりました。
Fargate採用にあわせ、セッション管理やHTMLキャッシュをmemcachedに書くように変更していますが、CodeIgniter2ではmemcachedにセッションを書けないという問題があったため、CodeIgniterは3系にバージョンアップしています。
フロントエンドのリニューアル内容
フロントエンドでは、jQueryとBootstrapを廃止したのが大きな変更点です。言語に関してもJavaScriptからTypeScriptに、CSSからSassを新たに採用しています。クラス設計にFROCSSを導入していることも特筆すべき変更点です。
また、npm-scriptsによるwebpackのビルド処理を取り入れることで、不要なリソースも含めて読み込みされていた問題を解消し、各ページの最適化と描画の高速化を実現しました。こうしたフロントエンドの変更は、リニューアル当時、入社してくれた新たなエンジニアが大きな貢献をしてくれています。
バックエンドのリニューアル
続いては、バックエンドです。先述の通り、データストア層をAPIにしたことから、デザインパターンを見直し、Modelを責務に合わせ、(API)ModelとPresenterに分割して記述する方法を採用しています。Presenterの振る舞いを以下に図で示します。
「開催されるパーティー」のデータ取得処理を例に、Presenterの振る舞いを説明しましょう。 Controllerは常にPresenter経由でデータを要求します。そしてPresenterは適切なデータストア層を判定しデータを返却するのです。ここでいう「適切なデータストア層」はCacheの有無によって判定され、Cache期間中であればCacheからデータを返し、CacheがなければModel経由でAPIからデータを取得して返す、という処理になっているのです。整理すると、
- Controllerの責務
- 必要データのリクエスト
- Presenterの責務
- どこからデータを取得するかを判定
このように責務の分断が可能になります。
また、Presenterにはもう1つの役割があります。旧システムでif文の複雑化に苦しめられた経験から、新たなシステムではViewにifロジックを書くことを禁止という制限を設けています。しかし、実際にはビジネス観点での契約上の表示の調整は必要になってきます。こうした調整事項を吸収するのも、Presenterの役割です。
例えば、何らかのクーポンが設定されてるパーティーとそうではないパーティーの参加応募フォームでは、当然ユーザーに表示するべき情報が異なります。旧システムでは以下のようにView側に責務があり、出し分けをされていました。
view/party_detail.php
<?php <h1><?= $party['title'] ?></h1> <?php if($party['coupon']): ?> このパーティーでは以下のクーポンが使用出来ます。 <div><?= $party['coupon']; ?></div> <?php endif;>
これに対し、新システムでは以下のように表示します。
view/party_detail.php
<!-- view側のコード --> <?php <h1><?= $party['title'] ?></h1> <?= $credit_view; ?>
view/parts/party_coupon.php
このパーティーでは以下のクーポンが使用出来ます。 <div><?= $party_coupon; ?></div>
party_presenter.php
public function get_coupon_view($party_coupon) { if ($party_coupon) { return ''; } else { return $thid->load->view('parts/party_coupon', ['party_coupon' => $party_coupon], true); } }
controller/party.php
public function detail($party_id) { $party = $this->get_party($party_id); $coupon_view = $this->party_presenter->get_coupon_view($party['coupon']); $this->load->view('party_detail', ['credit_view' => $credit_view]); }
責務がPresenterに移っています。View側はもらった$credit_view
の情報だけを表示し、ControllerはPresenterで生成された$credit_view
の情報をViewに連係することだけを行っています。どちらもパーティーにクーポンが設定されていてもいなくても、同じ振る舞いをしています。
こうした責務の分割は、一見すると「ViewのロジックをPresenterに移しただけ」にも感じられます。しかし、実はPresenterにビジネスロジックを閉じ込めることには、メソッドをテスタブルにする、という大きな意味があるのです。
メソッドの多くがテストしにくい状態、とは旧システムが抱えていた課題の一つでした。多くのModelやControllerは責務が大きくなりすぎたことで、Viewはif文の複雑化によって、テストが困難なものになってしまっていたのです。こうした課題を解決するべく選んだのが、「Presenterがifロジックを持つことで、各判断を疎結合にする」方法でした。PresenterはModelなので、ユニットテストの範囲でテストを書けます。以下の例のように、引数を渡して文字列が空文字か、そうではないか、という判定を入れること自体は非常に簡単です。
tests/model/presenter/Party_Presenter_Test.php
public function test_coupon_view_is_empty() { $party_presenter = $this->load->model('Party_Presenter'); $this->assertTrue($party_presenter->get_coupon_view('') === ''); } public function test_coupon_view_exist() { $party_presenter = $this->load->model('Party_Presenter'); $this->assertFalse($party_presenter->get_coupon_view('クーポン1') === ''); }
と、ここまでがコードにおけるリニューアルの概要です。より詳細な資料はそーだいさんによる『Webサービスの成長を止めずに リファクタリングする技術』のスライドが参考になるでしょう。
リニューアルによるエンジニアチームの混乱は小さかったと記憶してます。アーキテクチャ変更にあたっては、その設計思想を共有ドキュメントに言語化しておくことをお勧めします。新たなアーキテクチャを採用して1年ほど経ちましたが、設計当初の思想は時が経つにつれて風化しがちです。
つい最近も社内のコードレビューにおいて「Viewにif文があるのでPresenterに移したい」「Presenterに同じロジック持つ意味はなに?」という議論が生まれています。こうした場合でも、設計思想を記したドキュメントを参照することで、「if文はPresenterで持つべきだな」と納得できるのです。根本に立ち返ることが、非常に大事です。
見積もり困難な巨大プロジェクトをチームで遂行するためにやったこと
さて、ここからはフルリニューアルという巨大プロジェクトのマネジメントの部分に関して説明していきます。みなさんのご想像の通り、フロントエンド、バックエンド、インフラの全てを再構成することは困難が大きく、我々も決して順風満帆ではありませんでした。
ましてや我々は結成1年弱の開発チームで、オミカレのドメイン知識が豊富なメンバーは少なく、正しくコードリーディングをするのも難しいというハンデを追っている状態です。そんな状態の中でフルリニューアルを進めるにあたり工夫したことをお伝えします。
WBSを使い、リリースまでのマイルストーンを設定
このプロジェクトは全社を巻き込んだプロジェクトです。まずはプロジェクトを進めるにあたり、完了予定時期を見積もり、エンジニアチームにとどまらない、会社全体のロードマップを設定する必要がありました。
ただし、このようなバカでかいプロジェクトを正確に見積もるのは非常に困難です。そこで、まずWBS(Work Breakdown Structure:作業分解構成図)を作成し、マイルストーンを区切る手法を採用し動き出しました。
WBSでは、まず以下の作業を行います。
- 大きな粒度でプロジェクトの作業を洗い出して列挙
- 大きな粒度の作業をより小さなタスクに分解する
こうしたプロセスを経てタスクを洗い出し、抜け漏れをなくし、また、プロジェクト全体で必要とされる作業を明確化することで、見積もりの精度を高めるのです。後は
- タスクを機能ごとに区切り大きくマイルストーンを作成
- 各マイルストーンの完成スケジュールを設定
- 最終のスケジュールを決定
こうしたプロセスを経て見積もりを完成させます。
「そのマイルストーンはなにを実現するのか」を言語化する
設定されたマイルストーンは「α版」「β版」「RC版」「GA版」と名付けて分類し、それぞれがどのような状態を指すのかが言語化されチームにはいつでも見れる状態で共有していました。
それぞれのマイルストーンは以下のような状態と定義しました。
-
α版
- 「ゲスト会員における処理が単体で正常に動く状態」を指します。α版に含まれる機能は会員登録機能やパーティーの検索機能、口コミランキングの閲覧ページなどがあります。これら機能の完成をもって、α版の完了としました。
-
β版
- 「会員における処理が単体で正常に動く状態」を指します。会員は自分の予約やお気に入り情報を確認するマイページ機能を使えるので、マイページの作成と予約機能の実装が、β版での主な完了要件でした。
-
RC(Release Candidate)版
- 「各機能のバグを潰してシナリオテストが完了する状態」を指します。「会員登録→パーティーを検索し予約→会員ポイントが付与されているか」など複数のシナリオテストを進め、そこで見つかったバグを潰して安定版にした状態が完了要件です。
-
GA(General Availability)版
- 「リリース可能な状態」を指します。RC版との違いは共有ドキュメントに明記されていて、RC版と異なり、本番で動かすインフラのセットアップ(CloudFrontの導入など)や、キャッシュによってパフォーマンスを調整したものをGA版としました。なお、RC版とGA版の違いなどもドキュメント化し、メンバーには共有していました。
いま思い返してみると、社内共有ドキュメントに各マイルストーンと完成日を明記したことは、非常に大きな意味がありました。最大のメリットは、チームメンバーが現状のプロジェクトの進捗を鑑み、「α版の完成にはこの機能が足りないのでは」と、自身で判断できる土台を作れた点にあります。
私自身、プレイヤーとしてこのプロジェクトを進めるにあたり、今週何をすべきか、来週何をすべきかという、個人のtodoを考えるために、社内共有ドキュメントは何度も目を通しました。開発チームミーティングでもこのドキュメントを下敷きに、メンバーのタスクの割り振りが行われたのです。
もっとも、マイルストーンの設定は取り組みとしては有意義ですが、実際の開発がマイルストーン通りに進むかは別問題です。
α版の完成後、社内でテストを行ったところ、かなりの不具合報告が上がってきます。当初、α~β版の間にはバグ潰し期間を1週間ほど確保していたのですが、この期間ではとても潰しきれない量のバグが発生してしまったのです。こうした要因から、徐々にプロジェクトのスケジュール遅延が発生し始めます。
その後、β版のコーディングが完了したのは、RC版の完了予定日近くでした。さらに、β版からもバグチケットが上がってきてしまいます。とても最終リリース予定日に間に合う状況ではなく、リカバリのためのアクションが必須となります。そこでプロダクトオーナーやCTOの発案により、開発チームとビジネスチームの代表である弊社社長を巻き込んで「むきなおり会」を開催することにしたのです。
むきなおり会
「むきなおり会」とは書籍『カイゼン・ジャーニー 』で紹介されている手法で、現在向かっている先を確認した後、“どうあるべきか”を正す手法です。その手順を書籍から引用します。
- ミッション・ビジョンを点検する
- 評価軸を洗い出し、現状を客観的に見定める
- 評価軸ベースで「あるべき姿」と「現状の課題を洗い出す」
- 「課題解決」のために必要なステップを「バックログ」にする
- 「バックログ」の重要度と、一番効果の高いものを決める
- 時間軸を明らかにし、期限も明確に決める
『カイゼン・ジャーニー たった1人からはじめて、「越境」するチームをつくるまで』第16話より
多少、私たちの開発スタイルに合わせるためにアレンジを加えましたが、大きくは書籍で紹介されているステップの通りです。私たちの場合、以下のように結論しています。
- 「そもそもなぜフルリニューアルするのか」を開発チームメンバーで認識共有
- サービス(旧システム)のパフォーマンスに大きな課題がある
- 検索順位が徐々に落ちてきており、その対策のためにサービスの高速化を目的として設定した
- コードのリニューアルだけ完成させてリリースするという方策もある
- 本質的な目的を考えると「妥協できるポイントは少ない」という結論付け
「むきなおり会」の結果、「どうあるべきか」を変えない、つまり段階的なリニューアルは行わず、当初の目的通りフルリニューアルを実施する、という大方針が得られたのです。
大方針が決まれば、続いて取り組むべきは、リニューアル完遂のための現実的なスケジュール策定です。チーム全員で全ての課題をGoogleスプレッドシートに書き出し、そこからベロシティを指標にスケジュールを考えていく手法を導入します。
ベロシティとはスクラムにおいて1スプリントあたりにチームが消化した合計ポイントのことですが、私たちの場合は1スクラム=1週間
を基本単位とし、1チケット=1ポイント
としてベロシティを定義しました。
しかし、それまでは1チケット=1機能
としてチケットを定義していたので、新たな粒度のチケットへの移行オペレーションが発生します。そこで1チケット=1ポイント
となるように、現状のチケットを子チケットに分割するというルールを設定し、2スプリント(2週間)を回します。こうして算出したベロシティを用いて、最終のスケジュールを確定する、という対応です。
いま思えば、この手法は非常に良いものでした。従前ならば「タスクの消化」のためにタスクを消化してたチームメンバーが「具体的な終わり」に向けて手を動かすようにり、開発朝会の場でも、終わりが具体的に見えてきたことでモチベーションが回復した、という前向きな意見が複数のメンバーから出てくるようになったのです。
リリース後のシステムを支える仕組みづくり
ここまで紹介してきたのはリリースにいたるまでのクオリティと進行のマネジメントの取り組みです。プロジェクト終盤に差し掛かると、「リリース後」を視野に入れたマネジメントが必要になってきます。
私たちが取り入れたのは「改善Day」と「防火壁」という仕組みを導入しました。
リリース後の継続的改善を担保する「改善Day」
改善Dayは毎週火曜日と水曜日に、ランダムに2名のエンジニアを選び、自分の持ちタスクとは別の改善タスクをやってもらう仕組みです。改善Dayにアサインされる人は、前週の金曜日にSlackBotの機能を使い決定します。
改善Dayの導入には、フルリニューアル後もシステムを継続的に改善していく種まきという狙いがありました。重要だけど優先度が高くなく、担当がアサインされないタスクを「改善チケット」としてBacklogに切っておき、改善Day担当のエンジニアが当番の日にそのチケットを倒します。参考イメージは以下です。運用が始まった7月中に完了したタスクを載せているので優先度高ばかりが見えてますが、当時はちゃんと適切に振り分けられていたという事を伝えておきます。
改善Dayは今でも有効に活用されている仕組みです。例えば、Slackに来るエラー通知は危険度が低ければ改善チケットに回され、割れ窓の防止に役立っています。他にもAPIのフレームワークとして使用しているDjangoやTypeScriptといった、システムが使用しているOSSのバージョンアップ、また、5分ほどかかっていたgulpタスクを数秒で終わるように改良するなど、リニューアル後の運用改善に大きな影響を与えてくれました。
システム上の課題をうまく解決 / 改良する作業はエンジニアとしての腕の見せ所であり、チーム内での評価を獲得する良い機会として機能します。私自身、改善Dayの取り組みがチームで好意的に受け止められることも多いです。0→1が得意なエンジニアだけでなく、1→10が得意なエンジニアも成果を出せる仕組みとしても機能しているのです。
ユーザー対応と社内ドキュメント共有を促進する「防火壁」
また、改善Dayと同時に始まった防火壁という取り組みについても触れておきます。防火壁とはカスタマーサポートから寄せられる、突発的な技術的な問い合わせをさばく仕組みで、フルリニューアル後の「これは不具合では」というお問い合わせの増加が予想されたことから導入しました。
防火壁はアサインされると1週間丸々、防火壁担当となりカスタマーサポートチームからの全ての問い合わせに対応します。そのため、休みが多くユーザーからの問い合わせが多い週にアサインされると、その間は開発タスクがなにも進まない、ということもあり得ます。ですから、あらかじめ開発タスクの調整が必須で、属人化されがちな開発タスクを、強制的に別の担当に引き継ぐ仕組みでもあるのです。こうした引き継ぎ作業の副産物のように、社内共有ドキュメントが充実していきました。また、ドキュメンテーションの過程で洗い出された面倒ごとを改善Dayで倒すといった流れも醸成され、属人化解消と改善タスクの発見という、一挙両得の効果が得られたのです。
リニューアルによって得られたもの
デザイン、コード、マネジメント、さまざまな局面で紆余曲折を経て、2019年6月24日、当初のスケジュールから約1ヶ月の後ろ倒しを経て、フルリニューアルされたシステムがリリースされました。リリース後9ヶ月程度と、検証期間としては物足りなさはありますが、新システムを運用してきた中で感じる、リニューアルから得られた成果をお伝えしたいと思います。
1ヶ月の見積もりが2週間で実装に
比較的最近、SNSログイン機能の実装に関して見積もりをしていたところ、オミカレを古くから知るあるエンジニアは「ざっくり見ても1ヶ月は必要」と仮説しました。
旧システムでのユーザーの会員登録はライブラリへの依存度が高く非常に複雑になっており、私も旧システムのころの感覚に引っ張られ、彼の見積もった1ヶ月という時間は妥当に感じられました。しかし実際は、2週間程度で実装と動作テストまで完了したのです。
これには、新たに導入したPresenterが大いに役立ちました。Controllerは各種SNSごとに専用のPresenterにデータを連係するだけであり、Presenterは専用のParse処理などを実施する、といった個別の責務を持つような設計になっています。責務が複雑に依存しておらず、その分、新たな実装作業もスムーズに進められたのです。また、ControllerやPresenterは今後を見越してさまざまなSNSログインに拡張しやすいような設計とするなど、さらに今後の拡張性にもポジティブな影響をもたらしています。
また、SEOの観点ではmetaタグの決定を1つのPresenterとJSONに集約したことで、titleやdescriptionの修正コストが激減しています。アーキテクチャの恩恵によって工数が圧縮され、最も大きな目的としていた「新しい価値をユーザーに素早く届ける」が体現されつつあることが実感されます。
ユニットテストの導入
データストア層はAPIに寄せたことで、テスタブルな環境も実現しています。API側はDjangoのテスト機能が充実していることもあり、テストコードを書くことで動作が担保されます。新アーキテクチャでは、APIに“正しく”リクエストすればデータの保存は保証されます。
もっとも「APIに正しくリクエストする」部分が保証がされてない点は目下の課題です。現状では手動テストが多いですが、ここはSeleniumやCypressなどのE2Eテストの導入による自動化実現は今後の展望といえるでしょう。
また、ごく最近、PHP環境にはPHPUnitを導入しています。こちらも改善Dayの効果でPull Requestが発せられ導入にいたりました。今後はPresenterの導入目的の通り、主にModel層のユニットテストコードを書く、といった展望を持っています。
リニューアル後の構想
現在のオミカレでは以下のスライドにある「今後の構想」部分を進めています。
フルリニューアルでは主にユーザーが使用する部分を再構築しましたが、カスタマーサポートチームが使用する管理画面などはALBで旧システムのまま運用しています。管理画面には多くの重要な機能がありますが、反面テストが書きづらく、メンテナンスコストも高いという課題を抱えています。新たなアーキテクチャ同様、テストコードのあるデータストア層のAPIとそれを呼ぶPHP側という形でリニューアルを行っている最中です。
この移行が完了すれば念願のフルAPI化が完了します。合わせてDBリファクタリングが完了し、インフラコストを数十万円単位で削減できる見通しがあります。
まとめ
ここまでお付き合いいただき、ありがとうございました。オミカレのシステムフルリニューアルの裏側を、お伝えできるかぎりお伝えしてきました。
私たちのエンジニアチームは、各個人が自立自走できるメンバーが揃っていますが、反面、各個人が単独でタスクをこなすことが多いチームでした。しかし、このリニューアルは、初めて開発チームがチームとして機能したプロジェクトだったと思っています。局面局面での対応が果たして正解だったのか、もっと上手くやれたこともあったはず、と思うことは非常に多いですが、最後まで走りきれた結果には満足しています。
商環境や技術要件が変化していくスピードは凄まじく、その分、ユーザーへの新しい価値提供は素早く実現せなばなりません。価値提供を実現するシステムを作り出すことは、我々エンジニアに向けられた大きな期待だと、私は思っています。それに応えるべく、我々は絶えず研鑽を行わないといけません。今回の私たちの経験が、みなさんの研鑽の一部になり、技術的負債の解消、そしてユーザーへの価値提供のための助けになれば嬉しいです。
高橋一騎(たかはし・いっき) @ikkitang
*1:DBのリファクタリングに関しては、弊社の前CTOのそーだい@soudai1025さんによる、「DBの寿命はアプリより長い! 長生きするDBに必要な設計とリファクタリングを実践から学ぶ 」に詳細が記載されています。