こんにちは。テクノロジー本部のyoshikawaです。好きなLinux DistributionはManjaro Linuxです。
今回はレガシー化が進むLIFULLのメインサービスの開発効率の向上とコードベースの健全性の確保をすべく、Clean Architectureを採用しバックエンドを刷新している取り組みについて紹介させていただきます。
なお、Clean Architecture自体の説明および解説は本記事では行いません。
- 背景:歴史あるバックエンドの刷新
- アプローチ:新たなアーキテクチャと共創
- Clean Architectureの実践
- コンポーネントレベルでの規約:物理的なリポジトリ分割
- Clean Architectureを実践した所感
- おわりに:銀の弾丸はない
背景:歴史あるバックエンドの刷新
LIFULLのメインサービスであるLIFULL HOME'Sのバックエンドはその大部分がSymfony(PHP)ベースのモノリスと、ともにSinatra(Ruby)ベースのBFFとAPIサーバーの3層構造で構成されています。
このうち、Symfonyベースのモノリスは9年以上開発されており歴史のあるアプリケーションになっています。
長年の間多くのエンジニアによる開発が行われ、レガシー特有の問題が発生しています。
例えば、以下のような問題が発生しています。
- ビジネスロジックの複雑化、およびテンプレートエンジン(Twig)内のロジックとの混在化・密結合化
- APIおよびシステム内部のドキュメントが充実しておらず、I/Oがわかりにくい
- 異なるユースケース・開発組織(アクター)が利用しているなど、責務の不明瞭なモジュールが偏在している
このような問題の結果、開発効率の観点で以下の課題が生じています。
- 一度の変更が予期せぬ影響を与え得るため、新機能追加のための調査・設計・実装に時間がかかる
- 一度の変更による影響範囲が広く、機能改修のための影響範囲分析に時間がかかる
- 品質維持のための工数効率が悪い、バージョンアップがしづらい
こうした課題を解決すべく、バックエンドを刷新するプロジェクトが発足しました。
アプローチ:新たなアーキテクチャと共創
LIFULL HOME'Sの大部分のバックエンドは先述のSymfonyベースのモノリスに加え、ともにSinatra(Ruby)ベースの既存BFFとAPIサーバーの3層構造で構成されています。
この既存BFFにリファクタリングを行い、モノリスからビジネスロジックを移植することで開発効率の向上と健全性の確保を行うアプローチも考えられました。
検証の結果、モノリス上のビジネスロジックの移植対象をこの既存BFFではなく新しいBackend For Frontend(以降、新BFFと呼びます)に移植する、というアプローチによってバックエンドの複雑度を下げ開発効率を向上と健全性の確保を行うことになりました。
いわゆるストラングラーパターン(Strangler Fig Application)のアプローチに相当します。
現在は筆者を含めた数人のバックエンド刷新プロジェクトのチームメンバーが主体となって実装を進めていますが、今後はLIFULL HOME'Sに関わる多くのエンジニアと共に移植作業を行い、共創して刷新を遂行していく予定です。
採用したアーキテクチャ・技術
新BFFのアーキテクチャは「Clean Architecture」、言語は「TypeScript」、フレームワークは「LoopBack」を採用しています。
この3つの技術が選定された理由を紹介していきます。
Clean Architectureを採用した理由
採用した理由は複数あります。いくつか列挙すると、
- 著名かつ制約の厳しいアーキテクチャであり、実装の方言が生まれにくい。そのため多数のエンジニアが開発しても共通認識が持ちやすく。アーキテクチャの遵守が期待される
- DDDのパターン(レイヤードアーキテクチャ)の実装表現の一つであり、自己文書化をはじめとしたDDDの恩恵を受けることができる
- 実装の「詳細」を「抽象」に依存させることで、フレームワークやライブラリとの依存を減らし、バージョンアップを行いやすくすることが可能
- 物理的・概念的なレイヤー間の責務を明確にしやすく、
- オニオンアーキテクチャやヘキサゴナルアーキテクチャと比較すると書籍などの学習資料が充実している
などが挙げられます。
決め手となっていることは、最初に挙げたように「アーキテクチャレベルで明確な共通言語があること」です。
新BFFは長年に渡って多数のエンジニアによって開発されることが見込まれるため、健全性を保ったコンポーネントにするためにはアーキテクチャの共通言語を用意することは重要な観点でした。
一方で、Clean Architectureは学習コストが高く、その性質上Dto(Data Transfer Object)やInterfaceの実装量が増えたり冗長な実装が増えたりするというデメリットも存在し、開発効率向上には貢献しない可能性も考えられました。
この辺りの対処については後述します。
TypeScriptを採用した理由
言語のその他候補にはGolangやKotlinがありましたが、以下の理由からTypeScriptが採用されました。
- 漸進型付き/静的型付き言語により、これまでの開発環境にはなかった以下の強力な恩恵が受けられること
- データ構造が明確になることで、設計、実装およびIDEに頼ったリファクタリングのコストが下がる
- 型付けによって、APIドキュメンテーションの自動化が可能になること
- 多くのエンジニアが実装する(共創する)以上、学習コストの低い言語が望ましいこと
- フロントエンドと言語を一致させることで、学習コストの発生確率を下げて実装可能なエンジニアを増やせるといったシナジーが期待できること
決め手になっていることは「型による恩恵と学習コストの低さ」です。
これまでのLIFULL HOME'Sのバックエンドの言語はPHPとRubyであったこともあり、型付き言語の導入は開発効率向上に大きく貢献することが見込まれました。
また、Clean Architectureを採用している時点で一定の学習コストを計上しているため、JavaScriptのスーパーセットであるTypeScriptを採用することで学習コストを下げることは重要な観点でした。
LoopBackを採用した理由
LTSバージョンの期間、学習コスト、OpenAPIとの親和性、そしてClean Architectureとの親和性といった観点を考慮した上でLoopbackが選ばれました。特筆すべき点は以下です。
- OpenAPIのドキュメントが容易にホスティングできる(swagger)など、OpenAPIのドキュメンテーションのための機構が整っていること
- アノテーションだけで(View)ModelからJSON Schemaへの変換が容易にできること
- DI(Dependency Injection)が備わっていること
2つ目に挙げたJSON Schemaへの変換を取り入れた場合、UI層のViewModelがフレームワークに依存することを許してしまうことになるので、厳密にはClean Architectureの規約違反になります。
ただ、アノテーションのみの軽微な依存であるため許容するという判断になりました。
Clean Architectureの実践
Clean Architectureは設計の原則を提供しているものの、抽象度が高くそのままでは実装の自由度は高いままです。
具体的な新BFFでのレイヤー分け(=ディレクトリ構成)と、レイヤー内・レイヤー間の実装規約について紹介します。
レイヤー分け:例の図と新BFFアーキテクチャのレイヤーとのマッピング
こちらはRobert C.Martin氏が提唱したClean Architectureの図です。Clean Architectureを調べると一度は目にする有名な同心円ですね。 一方、以下が新BFFのアーキテクチャです。図中の色はClean Architectureの同心円の色と対応させてあります。
おおむね同心円の通りに各レイヤーと依存の方向を定義しています。
大まかなディレクトリ構成は以下のようになっています。
. ├── adapters // Interface Adapters │ ├── gateways // 外部DB等、内外のデータ形式変換レイヤー │ │ ├── datasources // Cacheや, Backend API接続用 │ │ └── impl // Data Access Interfaceの実装 │ └── ui // Controller, View, Presenter ├── application // UseCase/Application Business Rules │ ├── repositories // Data Access Interface │ └── usecases // Controllerと一対一に対応するUseCase ├── domain // Entity. Value Object, Domain Serviceも含まれる └── framework // フレームワークを拡張したもの
レイヤー内・レイヤー間:独自の規約を導入する
レイヤー間のBoundaryはどう実装するのか、フレームワークとの結合はどのように表現するのかなどレイヤー内・レイヤー間での規約も独自に導入しています。
その規約の中からいくつかの特徴的なものを列挙すると、
- RepositoryのInterface(Data Access Interface)をapplicationレイヤーに配置する
- 各レイヤーの境界を渡るデータ通信ではDto(Data Transfer Object)を利用する
- DtoはplainなTypeScriptのinterface
- UseCaseとControllerは原則一対一にする(アクターが混在する汎用的なUseCaseは避ける)
- これを実現するために、Controllerを細分化する
- ViewやControllerでOpenAPISpecification用の機構を利用し、フレームワークへの依存を一部許容している
- UseCaseではInputPortのみ実装するなど、冗長と思われるInput/OutputBoundaryは実装しない
といった点が挙げられます。Clean Architectureに準拠し守るべき原則は守りつつ、実装コストを下げるためにも適宜設計を変更しています。
規約違反の検知を自動化する
Clean Architectureの重要な規約の一つに、上位の方針が詳細に依存してはならない(=同心円の図における内側のレイヤーは外側レイヤーに依存してはならない)という規約があります。
この規約を守るようために紳士協定的にチェックリストを作成したり、人力のレビューで規約違反を防止のではなく、dependency-cruiserというライブラリを利用してCIに組み入れることで規約違反を自動で検知しています。
コンポーネントレベルでの規約:物理的なリポジトリ分割
新BFFは単一のGitリポジトリから構成されるような巨大なコンポーネントではありません。
以下の図のように、単一リポジトリ内でnamespaceを物理的にGitリポジトリを分割しmicroservice的に分割統治することで、一度に開発する開発者を限定することで開発効率の向上を図っています。
それぞれのGitリポジトリで前項までに紹介したClean Architectureベースのアーキテクチャが採用されています。
組織構造に追従したリポジトリ分割
Gitリポジトリは、新BFFが取り扱うドメインの違いによって分割しています。ドメインには賃貸、流通、分譲などのマーケット固有のものと、横断的な関心がありマーケットで分断することが難しいマーケット非固有のものがあります。
LIFULLでは取り扱うドメイン=マーケットをベースにして開発組織が構成されています。
つまりマーケット=組織構造に基づきGitリポジトリ=コンポーネントを分割することは、書籍『Clean Architecture』で述べられているようなコンウェイの法則の体現でもありますね。
前項までに述べたような、一つのコンポーネント内でClean Architectureを実践することによりモジュールレベルでの開発効率の向上と健全性の確保を図ることに加え、Gitリポジトリを物理分割することでそれぞれの組織が独立して開発可能になるようにコンポーネントレベルでも開発効率の向上と健全性の確保を図っています。
Clean Architectureを実践した所感
約1年前からバックエンド刷新プロジェクトが発足し、筆者は昨年5月末にプロジェクトにジョイン、そして昨年の8月ごろから新BFFの実装・設計に携わってきました。
それから今日までに得られたClean Architecture的な知見を書いていきます。
開発効率の向上とコードベースの健全性の確保は達成できたか?
これまでのモノリスと比較すると、設計、調査のためのコストは減少しており開発効率は向上していると思います。
これはClean Architectureを採用したからというより、PHPから型付き言語であるTypeScriptに移行したことにより、内部データ構造が明確になったこと、IDE(VSCode)を活用したリファクタリングがによる恩恵が大きいと感じています。
Clean Architectureを採用した影響についてですが、習熟度が低いうちは規約が複雑でレイヤーの責務がわかりにくく思えてしまいどのレイヤーにどの処理を書くべきか判断を誤ることもあり、かえって実装時間やレビュー時間の増加につながることもあります。
しかし、習熟度が高まれば解決可能な問題であるのでClean Architecture自体の性質に問題があるというよりは、それを継承している新BFFのアーキテクチャの啓蒙を進める必要があると考えています。
ただ単にモノリスからClean Architectureへの書き換えを行うだけでなく、実装可能なエンジニアを増やすことあるいはレビュー可能なエンジニアを育てることも当初の目的を達成するには必須と考えています。
BFFにClean Architectureの規約は複雑すぎないか?
前述した通り、少なくともClean Architectureを熟知したバックエンド刷新プロジェクトチームが制御できないような複雑さではないです。
設計・実装に悩んだ時はClean ArchitectureのルールとSOLID原則に立ち返って考えれば良いという根拠(=共通言語)が常にあるのは大きいと考えています。 レビューの根拠としての役割も大きいです。
とはいえ、現状の新BFFはクライアントからのクエリをバックエンドAPIに送信し、その結果にビジネスロジックを適用して返却するという参照系の処理が中心です。
そのため、単一のクライアントから単一のバックエンドAPIを呼び出すという処理を実現したい場合はPort、Adapter系をはじめとした抽象度の高い概念は、実現したい要件に対して実装の抽象度が高すぎるように思えてしまうなど、要件によっては規約が厳しいと思われるケースも存在します。
規約の遵守と開発効率の最適化
Clean Architectureを採用している以上、新BFFにおいてコードベースの健全性の確保のために規約を遵守すればするほど開発効率が落ち、開発効率を重視するほど健全性が落ちるというトレードオフと常に隣り合わせです。
冒頭で「共創」というアプローチをとっている、と述べた通り現在の数人のプロジェクトメンバーだけが新BFFの設計・実装を担うのではなく、LIFULL HOME'Sに関わる多くのエンジニアが新BFFの設計・実装を担う予定です。
将来的には累計で100人以上のエンジニアが開発に参加すると予想されるので、現状では冗長に思える実装があってもアーキテクチャを健全に保つ先行投資として規約の遵守を重視しています。
とはいえ、プロジェクト外のエンジニアの方から設計・実装面でのご相談・提案が発生していることもあり、規約の改善であったりアーキテクチャ自体を啓蒙し浸透させていく必要性も存在しています。
まとめると、
- バックエンド刷新のプロジェクトメンバーだけが遵守・理解可能な複雑すぎるアーキテクチャ・規約になること
あるいは、
- 実装の選択肢が多数考えられるような緩すぎるアーキテクチャ・規約になること
これらの両極端な結果になることで、Clean Architectureの設計原則が守られないアーキテクチャになることは避けたいところです。
ドメインモデリングが不足していないか?
Clean ArchitectureはDDDにおけるレイヤードアーキテクチャの実践パターンの一つであり、(Clean Architectureの)Entityを実装するにあたってはドメイン知識が整理されていることが必要となります。
しかしLIFULL HOME'Sのドメイン知識は整理されているとは言いづらく、ドメイン知識がドキュメントや実装に散在しているという状態です。
そのため、数値系のValue Objectを実装するにあたって、取りうる値の範囲を調べるためにDB仕様書や別のコンポーネントの実装を見なければならないというような事態が往々にして起こります。
そうした事態を防ぐためにもアーキテクチャのパターンとしてClean Architectureを採用する(いわゆる軽量DDDに陥る)だけでなく、DDDにおける戦略的設計を通じてドメインモデルを定義し集約し正しくDDDを実践していくことが重要だと認識しています。
現在は、DDDの思想の通りに改めてユビキタス言語を策定し実装各所に散らばったドメイン知識を集約・充実化するの取り組みが進行中です。
Clean Architectureを採用したのは正しいかった?
バックエンド刷新プロジェクトが発足してから1年が経過し、本番で稼働している新BFFも増えてきており新BFFのアーキテクチャも徐々に成熟してきました。
Clean Architectureを採用したのは正しいかった?という問いがあるとすれば、 開発者が増えた数年後に答えがわかる という回答になると考えています。
バックエンド刷新プロジェクトのメンバーのアーキテクチャへの習熟度は高いですが、将来プロジェクト外のエンジニアが実装するようになった時にこそ目的が達成できたかわかるるためですね。
その時に正しかったと言えるように現在鋭意開発を行っています。
今後導入したいこと
これまでに新BFFへと移植・リリースしてきたビジネスロジックはすべて参照系の処理でした。DBへのWrite処理が発生するような更新系処理の実装はありません。
そもそもLIFULL HOME'Sのほとんどの処理が参照系の処理であるためです。
単にソフトウェアエンジニアとしての興味もありますが、更新系の処理DDDにおける集約やトランザクションなどの観点でのプラクティスも確立していきたいところです。
例えば、現状はApplication層のみにあるData Access Interface(Repository)に対し、CQRS取り入れて参照系Data Access Interface(Query)と更新系Data Access Interface(Command)を作成することでDomainを洗練させていくといったアプローチがあるかもしれません。
実装効率を向上させるために
Clean Architectureの特性およびGitリポジトリを分割したことが起因して、新BFF全体で見ると冗長な実装が増えてしまっている箇所もあります。
例えば
- 大量のData Transfer Objectを作成する必要がある
- Dependency Injectionなどの定型実装が頻発する
- 同じようなValue Objectを複数Gitリポジトリ作成する必要がある
などが挙げられます。
前者2つはScaffolding Toolの作成・導入で解決できると想定しており、最後の1つはPackage(Github Packages)化することで解決できると想定しています。
しかし、どちらの解決策もある程度実装上のプラクティスが確立されることを前提としています。
Clean Architecture自体が抽象度の高いものでありそれゆえに実装の選択肢が多く、新BFFでの実装プラクティスも完全には確立できていません。
プロジェクト外のエンジニアの方々と円滑に共創していくためにも実装効率を向上させる取り組みは継続的に行っていきたいところです。
おわりに:銀の弾丸はない
開発効率の向上と健全性の確保を目指したバックエンド刷新プロジェクトはまだまだ進行中です。
Clean Architectureのわかりやすい解説や実装例を紹介した記事などの情報は存在しますが、実在するサービスで採用した事例や開発者の経験についてはあまり存在せず、投稿すれば有益な情報になるのではと思いこの記事を書かせていただきました。
銀の弾丸はない、という言葉はアーキテクチャ選定にも当てはまると思います。そのため、この記事をそのまま流用できるようなケースはあまり存在しないと思います。
今回紹介させていただいた中から転用可能なエッセンスを抽出して、技術的負債の解消や何年も続くことを見越した新規サービスの技術選定の際に役立てていただけると幸いです。