こんにちは、R&D 推進室の佐草(@rendaman0215)です。
現代のソフトウェア開発において、システムの柔軟性、スケーラビリティ、そして保守性を高めることは重要な課題ですが、弊社ではモノリスな環境で運用しているシステムも多いです。
そんな中、直近携わったシステムで疎結合なアーキテクチャを設計したことで、多くのメリットを享受することができました。本記事では、その設計/実装プロセスと効果について詳述します。
疎結合アーキテクチャ
疎結合アーキテクチャといえば、システムの各コンポーネントが他のコンポーネントに強く依存せずに機能する設計を指します。 これにより、システム全体の柔軟性、スケーラビリティ、保守性が向上します。
弊社が最近作成してきたシステムは、フロントエンドとバックエンドこそ独立しているものの、API 部分で担う責務が多くモノリスに近い形となっています。
既存システムは大変大きくなっており、手をつけるにはリスクもあるため、 今回新規で立ち上がったプロジェクトでこれらのアプローチを行い、各プロダクトに浸透させていくことが狙いです。
既存の課題
弊社で運用しているモノリスなシステムには多くの課題がありました。
以下は一例ですが、開発の現場ではこれらの課題がありました。
- テストコードが書きづらいため、カバレッジが下がり、テスト工数が膨らむ
- 各ロジックが散り散りになっており、変更箇所を特定するのに時間がかかる
- I/O 部分が密結合となっているため、交換する際に時間がかかる
- 認知負荷と責任範囲が大きくなることでメンバーのパフォーマンスが落ちる
- プロダクトのオーナーシップの所在が不明
- 開発規模が 10 人以上となるようシステムが多く、スクラム開発などの フレームワークとの相性が悪い
アプローチ 1: クリーンアーキテクチャ
アプリケーション設計的なアプローチとして、クリーンアーキテクチャを導入しました。
参考になったのは、みんな大好きクリーンアーキテクチャ本です。
ディレクトリ構造の設計
ディレクトリ構造は以下のとおりとなっており、基本的にクリーンアーキテクチャに則り作成しましたが、
弊社で扱っている Golang の PJ における基本形を破壊的に変えることなく移行しています。
. └── src/ ├── cmd/ │ └── main └── internal/ ├── domain/ │ ├── models/ │ │ └── 各モデルを定義 │ └── repositories/ │ └── 各モデルの振る舞いのIFを定義 ├── services/ │ └── ビジネスロジック系ファイル └── infrastructure/ ├── api/ │ ├── httpClientに関連するファイル │ └── httpClientを利用してrepositoryを実装 ├── databases/ │ └── mongo/ │ ├── mongoDBに関連するファイル │ └── mongoDBを利用しrepositoryを実装 └── mock/ └── モックを用いてrepositoryを実装
次に各ディレクトリの役割について説明します。
/domain
このシステムの根幹と概念をまとめたディレクトリです。
models
では各モデルの定義、例えば原稿モデルにはプロダクトタイプ(バイトル、はたらこねっと、バイトル PRO、etc..)があるなどの定義です。
repositories
では各モデルに対してどういったアクションをとるかを定義しており、例えば原稿を取得する・更新するなどがあります。
/infrastructure
外部接続系のファイルをまとめたディレクトリで、repository のインターフェースを実装するファイル群となります。 database, api, mock など情報取得元や方法が異なるものをそれぞれ定義しており、 main で注入する際に、好きな infrastructure を選んで注入することが可能となっています。
/cmd
このディレクトリにある main 関数で上記で定義した infrastructure を注入できるようにしています。
アプローチ 1 で解決した課題
上記の設計でクリーンアーキテクチャを意識したことにより結果的に以下の恩恵がありました。
- データストアの接続部や外部 API 連携部、ビジネスロジックなどがどこにあるか一目でわかるようになり、修正箇所が特定しやすくなった
- I/O 部分のみ取り替えた別のシステムをすぐに構築できるようになった
- ひとつの関数で依存するものが減り、テストコードが書きやすくなった
- カバレッジがあがり、テスト工数を抑えることができた
- Github Copilot の筆がのるようになり、楽できた
アプローチ 2: API の分割
従来の弊社のシステムではフロントエンドと API が 1 対 1 になることが多かった弊社のシステムですが、 セキュリティの関係で今回は 以下の通り API を 2 つに分割することになりました。
- 既存の DB に繋ぐ API
- ビジネスロジックを集約した API
疎結合なシステムを意図しないで始めたこの設計ですが、独立性を意識して維持することで結果的にさまざまな課題を解決しました。
データストアは隠蔽
データストアの隠蔽は、マイクロサービスアーキテクチャにおいて非常に重要な考え方です。データストアを各サービスに閉じ込めることで、サービス間の結合度を低く保ちます。これにより、各サービスが独立してデプロイ可能となり、スケーラビリティと信頼性が向上します。例えば、サービス A が MongoDB を使用し、サービス B が PostgreSQL を使用していても、互いに影響を受けることはありません。データストアの隠蔽は、各サービスが独自のデータモデルを持ち、サービスのインターフェースを介してのみデータを操作することを促進します。
ビジネスロジックをもつ
ビジネスロジックを各サービスに持たせることは、変更の柔軟性を高めるために重要です。ビジネスロジックをこのサービス内に集約することで、特定のサービスに変更が生じた場合でも、その影響を最小限に抑えることができます。例えば、在庫管理サービスが在庫の計算ロジックを内部に持つことで、販売サービスや配送サービスと独立して改修を行えます。これにより、開発チームは特定のドメインに集中でき、変更の迅速な反映が可能となります。
アプローチ 2 で解決した課題
分割したことで担当範囲が広がった : API を細かく分割したことにより、チームは特定のサービスにフォーカスし、そのサービスに関わるストリーム(設計、実装、テスト、リリース、運用)を一貫して担当することができました。これにより、専門知識が深まり、効率的な問題解決が可能となりました。
認知負荷と責任範囲が小さくなることでメンバーのパフォーマンスが上がる : 各サービスの責任範囲が限定されることで、メンバーの認知負荷が軽減され、効率的に業務を遂行できるようになりました。これにより、個々のメンバーがより高いパフォーマンスを発揮しやすくなりました。
システムあたりの開発規模が小さくなり開発フレームワークと相性がよくなった : サービス分割により、各サービスの開発規模が小さくなり、スクラムやカンバンなどのアジャイルフレームワークと非常に相性が良くなりました。小さなサービス単位での計画とデリバリーが可能となり、リリースサイクルの短縮と迅速なフィードバックループが実現しました。
まとめ
今回の取り組みで、クリーンアーキテクチャと API の分割を導入することで、システムの柔軟性や保守性を向上させることができました。特に以下の点で大きな効果を実感しました:
- 修正箇所が特定しやすくなった
- テストコードが書きやすくなった
- 認知負荷と責任範囲が小さくなることでメンバーのパフォーマンスが上がる
改善したことはたくさんありましたが、気を抜くとモノリシックなシステムになってしまいそうですのでここには引き続き注意していきたいと思います。
今後も、このアプローチを他のプロジェクトにも適用し、さらなる改善を目指していきたいと思います。