こんにちは! LIFULLエンジニアの吉永です。
普段はLIFULL HOME'SのtoC向けのCRMチームにてエンジニアリングマネージャをやっています。
本日はクリーンアーキテクチャで構築したプロダクトが初版リリースから2年経過した現在、どうなっているか?について紹介したいと思います。
これから新規プロダクトにクリーンアーキテクチャを採用しようとしている方々の参考になれば幸いです。
アジェンダ
クリーンアーキテクチャで構築したプロダクトの概要
今回のブログで紹介する我々のプロダクトですが、オムニチャネル戦略を推進する為の各種機能を提供するAPIサーバーとなり、下記のような機能を持っています。
- LINE/Mailでその日の新着物件情報をユーザーが希望した検索条件に応じて通知する機能
- LINE/Mailでユーザーが問合せた物件に近い条件の物件をレコメンドする機能
- LINE公式アカウントでユーザーとトークルームでインタラクティブなやりとりを行い、シナリオ配信やレコメンド物件配信を行う機能
- 各種サービスとSalesforce社のService CloudやMarketing CloudとのAPI通信を仲介する機能
- 言語はGoを採用
これらの機能を構成するにあたりクリーンアーキテクチャを採用しており、有名な下記の図にならった形で各レイヤーを構成しています。
なお、このプロダクトにクリーンアーキテクチャを採用した当時はチーム内でクリーンアーキテクチャ及びGoの実装を経験したメンバーはおらず、書籍やWeb、社内で先行採用されていたプロダクトの実装を参考に見様見真似で実装しました。
開発初期は実装もレビューもキャッチアップに時間がかかっており、新しい言語、新しいアーキテクチャによる実装で非常に苦労したことを覚えています。
クリーンアーキテクチャを採用して得られたメリットやデメリットについて
どんなアーキテクチャにもメリット・デメリットはありますが、クリーンアーキテクチャにおいても同様で、実際に採用してみて個人的に感じたことを紹介します。
メリット
- 各レイヤーの責務をアーキテクチャで示してくれているのと、各レイヤーはインターフェースに依存するように設計するので自ずと各レイヤーが疎結合になり、ユニットテストを行いやすい。
- ユニットテストを行いやすいので、テストコードを実装するのがあまり面倒にならず、結果的に変更に対する品質の担保をしやすい為、安心して変更できる。
- レイヤー間の独立性が高いので、インタフェースさえ設計してしまえば、各レイヤーの実装を別々の担当で並行に作業を進めやすい。
- クラス設計に迷ったときにクリーンアーキテクチャという指針があるので、チーム内での議論があまり散らばらず収束しやすい。
- 指針は示してくれているが、かと言ってガチガチに固まっているアーキテクチャではないので、ある程度は自分たちの開発に特化している形に実装してしまっても、大枠のレイヤー構成や依存ルールさえ守れていれば拡張性と保守性を高く保つことができる。
- レイヤーが疎結合になることで、各レイヤーの実装のシンプルさが保たれるので、ソースレビューしやすい。
デメリット
- 小さな機能追加でもアーキテクチャ内の登場人物が多いので、クラス数が増え、煩雑になりやすい。
- 基本的にインターフェースに依存するので、IDEでメソッドの「定義へ移動する」際に、たいていのIDEでは定義だとインタフェースに飛んでしまい、メソッドの実態へ飛ぶには「実装へ移動する」を選択する必要があるので、コード内の移動に少しだけストレスを感じることがある。※それはあなたのIDE環境の問題でしょうというご意見もありそうですが
- エンティティレイヤーは「ビジネスルールをカプセル化したメソッドのあるオブジェクトやデータ構造と関数の集合」を実装するレイヤーなので、DBの1レコード分のデータをマッピングするだけで、アクセッサー以外のメソッドを持たない単純なオブジェクトと複雑なビジネスロジックを実装したオブジェクトが混在することもあり、適切にパッケージなどで分けないと煩雑になりやすい。
2年経過してみて今どうなの?
ステップ数
初版リリース後、様々な機能を追加していったので、リポジトリ内のステップ数は2年間で約3倍になりました。
テストカバレッジ
初版リリース時は自動テストコードのカバレッジは80%程度でしたが、初版リリース後3ヶ月ほどかけて98%まで上昇させ、その後現在に至るまでこの水準を維持しています。
これは、新規追加ファイルのテストコードもなるべくカバレッジは100%を目指そうという方針で開発を進めてきたので、チームの皆で意識して取り組んでいる結果、維持できていると思います。
レビュー
新しくエンドポイントを実装する際はまずクラス図をクリーンアーキテクチャの各レイヤー図にあてはめて作成し、各レイヤー間のインタフェース仕様と追加・変更するクラスを設計レビューしています。
この時点で大枠の設計を固めており、以降の実装フェーズでは基本的にどのレイヤーから実装しても良いようになっている為、実装者が手を付けやすい個所から実装することが出来ています。
メリットでもあげましたが、レビュアーからすると各レイヤーが疎結合でシンプルに実装されている為、レビューはしやすいと思います。
まだ本格的には運用できていないのですが、直近ではPRの変更行数をなるべく抑え、レビューしやすい粒度にPRを分割しようという取り組みもあり、以前だと新規エンドポイント実装時はコントローラー層からドメイン層まで一気に実装してからまとめてレビュー依頼をしていましたが、今後はコントローラーとコントローラーのテストコード、テストコード内で利用するユースケースのモックで1PR、それが終わったらユースケースとユースケースのテストコード、テストコード内で利用するリポジトリやドメインのモックで1PRというようにある程度レイヤーで区切ってのレビューを試してみて、レビュー負荷の軽減ができそうかを検証していこうと思っています。 ※もともとクリーンアーキテクチャで実装されたソースはレビューしやすいと感じていたので、1PRで確認する対象が減ると見逃しも減り、対象へより集中してレビューできると思うので、PRサイズの削減を検討しています
改修のしやすさ
改修はしやすいです。
理由としてはメリットでも挙げた、単体テストを実装しやすい為、既存のコード変更があまり怖くなくなっていることと、各レイヤーが独立している為、例えば新規でエンドポイントを追加する際にも、既存のリポジトリやユースケースのインタラクターを再利用しやすく、コントローラーだけを新規で作成すれば良いといった場面もあり(もちろんその逆もありますが)、インタフェースに依存するようにプログラミングしておくことのメリットを最大限享受できていると思っています。
また、言語も静的型付けであるGoを採用していることもあり、各レイヤー間のインタフェース変更も型が一致しなくなってコンパイルエラーになってくれるので、改修漏れが発生しないことは良いことだと思います。
直近だと、分散トレーシング対応をする為に、コントローラーからエクスターナルまでContextオブジェクトを伝搬させる為のリファクタリングを進めているのですが、コンパイルエラー除去と単体テストの修正を行えば安心してリリースできる状態であることが、開発者がストレスなく開発できていて、開発者体験向上にも一役買ってくれていると実感しています。
余談になりますが、LIFULLではKEELというプラットフォームで分散トレーシングする為の基盤を提供してくれていて、アプリケーションレイヤーで少しだけコードをいじるだけ簡単に複数サービスのログを統合して閲覧できるようになっているので、下記の記事も良かったら見てください。
まとめ
クリーンアーキテクチャを採用したプロダクトが2年経過してみて今どうか?について紹介しました。
正直ネガティブな感想はあまりなく、今後数年に渡って改修を続けていってもあまりゆがまずに済むのではないか?と思っています。
もちろん、初期構想時と比べると、いくつかパッケージ分けが適切じゃないかもとか細かい個所で修正したい個所はあるものの、大枠でみていくとクリーンアーキテクチャで実装したからこそカオスにならずにある程度秩序が保たれていると思います。
また、クリーンアーキテクチャで実装することで、SOLID原則に自然と準拠していき、今まで理解があやふやだったSOLID原則が少し理解が深まったと思うので、そういった面でも一度触れてみることは良いと思います。
これからクリーンアーキテクチャを採用してプロダクトを開発しようとしている方々へ少しでも参考になれば幸いです。
最後に、LIFULLでは共に成長できるような仲間を募っています。
よろしければこちらのページもご覧ください。