見出し画像

「アーキテクチャconference」に登壇しました【イベントレポート】

こんにちは、deep-rain.com(@deepraincom)です。
2024年11月に行われたFindy主催の「アーキテクチャconference」に、「物流システムにおけるリファクタリングとアーキテクチャの再構築 〜依存関係とモジュール分割の重要性〜」というタイトルで登壇させて頂きました。登壇時に話しきれなかった内容も含め、出来るだけ詳細にお話させて頂きます。


自己紹介

2024年2月入社以来、CTO室というチームで日々全社規模の技術課題と向き合っています。
CTO室は、これまでオープンロジの10年という歴史を経る中で、積み上がってきた技術負債や、生産性向上などの課題に向き合うために発足したチームであり、高い専門性を持つメンバーが在籍しています。


登壇スライド


概要

このセッションでは、「物流システムにおけるリファクタリングとアーキテクチャの再構築 〜依存関係とモジュール分割の重要性〜」をテーマに、10年の歴史を持つ物流システムのパフォーマンス向上を目的とした取り組みをご紹介しました。

変更容易性と拡張性を重視し、シンプルなアーキテクチャを構築した実例を、具体的な手法とともに赤裸々にご紹介していきます。


プロジェクトの背景

最初は簡潔で変更しやすいシステムであっても、仕様の追加や変更を繰り返すうちに構造が癒着し、変更の影響範囲が拡大していきます。結果として、徐々に変更容易性が損なわれます。

また、どれほど優れた構造であっても、技術の陳腐化や組織構造の変化によるコンテキストの喪失などにより、構造の価値が毀損されることは避けられません。

今回は、影響範囲を限定することで変更容易性を確保すること、さらに今後も構造の価値を維持しやすいアーキテクチャを実現することを目的としています。

パフォーマンス課題の表出

今回、構造を見直す主なきっかけは、「引当処理」のパフォーマンス改善でした。当初はSQLのチューニングなど、一般的な取り組みを行い、性能向上を目指していました。

しかし、それだけではパフォーマンス要件を満たすには不十分であり、抜本的なコードの変更や処理の仕組みそのものを見直す必要がありました。

「引当処理」は非常に重要な処理であり、安全な変更を行える仕組みが求められる一方、これからの取り組みでは頻繁な変更も避けられません。
しかし、現状の構造では小さな変更にも多大なコストがかかるという課題が浮き彫りになっています。

抜本的なパフォーマンス改善を実現するためには、構造の価値を底上げし、変更コストを大幅に下げる必要があることが明確になりました。


構造の価値に投資することの意思決定

ソフトウェアの構造に価値を見出し、それに投資する意思決定を行うようにステークホルダーを説得するのは、ソフトウェアエンジニアの重要な責務です。なぜなら、構造がビジネス価値に直接的な影響を与えることは少なく、その価値を理解できるのはソフトウェアエンジニアだけだからです。

現実には、構造への投資よりも、事業のコアバリューに直結するソフトウェアの振る舞いにリソースを投入する方が価値が高いと見なされがちです。実際、それが正しい場合も少なくありません。

今回は、事業価値を高めるうえでパフォーマンスに課題があるという明確な問題と、その改善に小さな変更でも数ヶ月を要する事実に基づき、「最低限必要な変更容易性を確保するため、構造に投資することが最短の解決策である」ということが言語化・可視化され、議論は非常にスムーズに進みました。

逆に言えば、「言語化・可視化できない課題」に対して「構造の価値に投資しましょう」と提案しても、事業価値に直接的な影響が見えないため、ステークホルダーはおろか誰にもその価値が伝わりません。これでは意思決定が滞るのは当然です。

意思決定の鍵は常に事業価値の最大化にあります。極端に言えば、事業価値に繋がらない投資を営利企業が行うのは、よほど余裕がない限り非常に難しいと言わざるを得ません。


コードの量と認知負荷

ひとつのファイルに対するコードの量が認知負荷に直結するわけではありませんが、少なくともその傾向はあります。
人間が一度に扱うことの出来る情報量は限られています。たとえば、 JavaScript で let よりも const が推奨されているのは、変数が変化しないことを保証することで、開発者が変数の状態を追跡するための認知的負荷を軽減している、と見ることもできます。

認知負荷とは、誤解を恐れずに言えば、「何かを達成するために脳が処理・記憶し続けなければならない情報量」のことです。

コード量が長くなればなるほど、変数や計算などのあらゆるコードがコンテキストを持ってしまい、結果として脳のリソースを占有してしまいます。

今回の重要なテーマのひとつであるモジュール化は、こういった認知負荷軽減による可読性向上も見込んでいます。モジュールに適切な役割と責務を持たせ、処理を可能な限りシンプルにすることで、一度に読まなければならないコードの行数を数十行に留めることができ、結果として可読性が向上する、というわけです。


リファクタリング戦略とアーキテクチャの設計

オープンロジのシステムも多くの例に漏れず、フレームワークに依存したMVCアーキテクチャを採用していました。

MVCアーキテクチャは、Model + View + Controllerから成るシンプルで実績のある構造で、多くのWebフレームワークで採用されています。その効率性は魅力的ですが、一方で、システムが複雑化すると影響範囲が広がりやすく、各レイヤーが肥大化しがちというデメリットがあります。

このレイヤーの肥大化を防ぐために、Serviceレイヤーなどを導入するのは、一般的な解決策です。しかし、そのServiceレイヤーが再び肥大化するという問題に直面し、いたちごっこの様相を呈することも少なくありません。

とはいえ、レイヤーの肥大化自体は今回の直接的な課題ではありません。原因の一端である可能性はありますが、本質的な課題は「変更容易性の欠如」です。

つまり、肥大化したサービスクラスを縮小させることが目的ではなく、「変更したい部分を変更しやすくする」ことこそが、最優先すべき目的なのです。


目的の明確化

先に述べたとおり、「変更容易性の確保」が最優先の目的です。しかし、「変更容易性が確保されている状態」とはどういった状態なのでしょうか?

このような抽象的な目的を掲げる場合、実際のソリューションに落とすためには、もう少し具体的な目標に分解していく必要があります。「何が達成されていれば最終的な目的を達成したと言えるのか?」といった質問に答えられるような形が理想的です。OKRのようなものをイメージして頂けるとわかりやすいかもしれません。

ここでは、目的を「安定性」「開発効率」「安全性」という3つのキーワードに分解し、これらを全て達成できる構造が、「変更容易性が確保されている状態」を達成している状態である、と定義します。

抽象的な目的を分解し、具体化していくというプロセスは、システム開発に限らず、あらゆる事柄に応用できます。目的に向かって明確な道筋を立てることで、迷いなくシンプルなソリューションを模索することが可能となります。


依存の方向とモジュール化の重要性

依存の方向の明確な定義は、モジュールに対し、システムにおける安定性と柔軟性を決定づける行為であると言っても過言ではありません。

ここで重要なのは、依存の方向を揃えることです。依存されないものと、依存されるものは明確に切り分けて定義することで、影響を最小限に抑えることができます。これによって、柔軟に変更できるモジュールを作成することが目的です。

クリーンアーキテクチャや、レイヤードアーキテクチャなどをはじめとする、いくつかのアーキテクチャパターンも、この依存の方向の重要性を強調しています。

依存の方向を定義するには、適切なモジュール化も重要です。
意味のある単位で機能や処理を切り分け、適切な責務を持たせることで、モジュールという単位に処理をまとめることができます。これによって、モジュール同士の結びつきを依存という概念で表現し、さらに方向という概念を持たせることができるようになります。


モジュールと依存の設計

それでは、実際に、引当処理をモジュール化し、依存の方向を揃えていくように設計を行ってみましょう。

処理の分解

今回のテーマとなる「引当」という機能は、大きく分けて4つの処理に分解できます。

  • 引当可否判定

    • 引当処理を行って良いか、優先度が高いほかの注文がないかを確認

  • 在庫情報取得

    • データベースなどから在庫情報を取得する

  • 割当アルゴリズム

    • 在庫情報と注文情報を突合し、実際に割当を行うアルゴリズム

  • 結果の永続化

    • 結果をデータベースなどに永続化する

これらに対し、それぞれ変更容易性を確保していくことが今回の目的です。


インターフェースの重要性

今回は、依存の方向を変化させる手段としてインターフェースを利用します。

インターフェースは、多くのプログラミング言語でサポートされている一般的な機能ですが、その価値が理解されにくい側面もあります。インターフェースがなくても困らず、抽象クラスや継承ほど明確なメリットがないように見えるからです。

しかし、インターフェースには「どのような振る舞いを持つべきか」のみを定義できるという独自の利点があります。実装を持たないため、それ単体では動作しませんが、この特性が「振る舞いにのみ関心を持ち、実装に依存しない」設計を可能にします。

JavaのListインターフェースなどは分かりやすい事例で、インターフェースは List という安定的な仕様を定義しており、これが変更されることは滅多にありません。しかし、実際にはそのユースケースによって、有効なアルゴリズムは異なります。たとえば、ArrayList と LinkedList はそれぞれ得意なユースケースがあり、誤った選択をするとパフォーマンスが大きく劣化します。
また、インターフェースが仕様書の役割を担っていることも着目すべきポイントです。先程挙げたListインターフェースのコードを見てもわかるとおり、クラスと各メソッドにはコメントによって明確な仕様が定義されています。これは散逸しやすい仕様書を適切に管理するための大きなヒントになるでしょう。

Listインターフェースの例で言えば、List というインターフェースは、明確に仕様を示しており、安定している。つまり、「依存される側」である一方で、 ArrayList や LinkedList は、List に依存しているが、他から依存されているわけではありません。つまり、柔軟に実装を変更したり、差し替えたりすることが出来る、ということになります。


インターフェースの定義

インターフェースは、安定性を提供し、モジュール同士を疎結合化させるための有用な概念です。
先ほど分解した4つの機能に対し、インターフェースを定義することで各モジュールの疎結合化を実現していきましょう。

ここでは、モジュールを繋ぐインターフェースとして以下を定義しました。

  • 注文

  • 在庫情報

  • 引当結果

青色で示されている各処理は、緑色で示されているインターフェースに依存していますが、各処理同士の依存はなく、疎結合化を実現できています。

インターフェースを安定させることで、各処理を差し替えたとしても他の処理に影響を及ぼさない構造となっており、少なくとも各処理においては変更容易性を実現できたと言えるでしょう。


テスタビリティの向上

各処理を分割し、インターフェースを定義することによる効果は他にもあります。仕様が安定することで、処理に対するテストが容易になる点です。

安定したインターフェースがあれば、テストはインターフェースに依存すれば十分であり、実装を基にテストケースを作成する必要はありません。重要なのは、インターフェースの規約を満たしているかどうかだけです。そのため、仕様に則ったテストケースを簡単に作成できるほか、「インターフェースの規約を満たすスタブやモック」に差し替えても問題が生じません。

テストに関しては本題から少し離れるため詳細は割愛しますが、適切なモジュール化と依存の管理がテスタビリティを向上させる点は大きなメリットです。


フレームワークとの共存

フレームワークは、ソフトウェア・アーキテクチャの文脈ではあまり意識したいものではないかもしれません。
しかし現実的には、フレームワークに頼らず開発することは稀ですから、多くの場合は適度な距離を保ちながら共存する道を模索することとなります。

フレームワークのレイヤーもひとつのモジュールである、という解釈で、今回モジュール化する機能に対して依存の方向を整理してみましょう。

先ほど設計したモジュール構成をここに当てはめてみましょう。
Modelの変更が影響を与えるのは「結果の永続化」と「在庫情報取得」の処理に限られます。一方で、割当アルゴリズムや引当可否判定には影響を及ぼしません。

さらに、青色で示した各処理を差し替えた場合でも、緑色の在庫情報や注文といったインターフェースが守られている限り、他の処理に影響を与えることはありません。

このように整理できていれば、たとえばモジュールをマイクロサービス化したり、イベント駆動型のアーキテクチャを採用する場合でも、必要な手間は大幅に軽減されます。これにより、従来のアーキテクチャと比べて柔軟な変更が可能となり、将来の選択肢が増えたと言えるでしょう。


設計の心得

プロジェクトによって適切な設計は異なりますが、今回は変更容易性の確保という目的のために、可能な限りシンプルな構造を設計しました。

チーム開発では、活発な議論やコミュニケーションが非常に重要です。コードや構造も同様で、議論の妨げとなる要素はできるだけ排除する必要があります。もちろん、チーム全体で共有できる概念があれば、それを活用することに問題はありません。ただし、DDDやクリーンアーキテクチャといった概念は複雑かつ難解であり、キャッチアップには相応の時間がかかります。

少人数のプロジェクトでは、優れたアーキテクトが設計をリードすることで、学習コストを払っても十分なメリットが得られる場合があります。ひとつのチームで濃密なコミュニケーションが可能な段階では方向性を揃えられますが、規模が大きくなるとそれは難しくなります。

特に、ソフトウェア・アーキテクチャのような抽象的な概念のパターンは、解釈やスキルの差が生じやすいため、学習コストに注意を払う必要があります。

ここで大切なポイントを3つご紹介します。

1つ目は『言葉をむやみに増やさないこと』です。
ここで言う「言葉」とは、一部のコンテキストでしか使われない新しい専門用語を指します。
言葉は、その概念に習熟しているメンバーのコミュニケーションツールとして非常に有用な一方で、そうでない人にとってはスムーズな理解を妨げたり、本質的でない誤解を与える要因となります。

2つ目は『不要なパターンや概念を取り入れないこと』です。
学んだパターンや概念は思わず使いたくなってしまいますが、目的を達するために必要ない概念を無理に適用しないことが大切です。シンプルさを維持することで、設計が柔軟になり、長期的なメンテナンスも容易になります。

そして最後に『目的を定義すること』です。ソフトウェア・アーキテクチャは抽象度が高く、唯一の正解があるわけではありません。
陥りがちなのは、綺麗でリッチなアーキテクチャを構築したものの、目的を満たさず自己満足に終わるケースです。

構造はあくまで手段です。目的を明確に定義し、その目的に合った手段を選ぶことが肝心です。


リファクタリングの進め方

設計した構造も、実際のコードに落とし込まなければ絵に描いた餅に過ぎません。ここからは、実例をもとに、実践的なリファクタリングの進め方をご紹介します。


インクリメンタルな改善戦略

巨大な変革を一度に行うのは魅力的に見えますが、現実的にはリスクが高く、成功は困難です。特に大規模なシステムやプロジェクトでは、全てを一気に変更すると、未完成な部分や予期しない問題が発生しやすくなります。

これを防ぐには、スコープを区切り、少しずつ変更を加え、細かくリリースするアプローチが効果的です。

地道に進めながら確実な成果を積み重ねることで、プロジェクトを成功に導くことができます。


テストの追加

引当処理のような事業の存続に関わる重要な機能では、安全性の確保が不可欠です。

そのため、既存の単体テストの拡充に加え、実データを基にあらゆるパターンを分析し、包括的かつ広範なテストが行える仕組みを構築しました。

具体的には、過去数ヶ月分のアクセスやデータのパターンをキャプチャし、擬似的にリクエストと結果をシミュレートする仕組みを構築しました。この仕組みにより、コードが仕様を満たしているかは確認できませんが、変更前後で結果が変わらないことを一定程度保証できます。

これにより、単体テストでは拾いきれないパターンを検証可能とし、リファクタリング時の振る舞いの変更を低コストで検知できるようになりました。


コミュニケーション設計

構造設計では抽象的な議論が重要ですが、最終的に必要なのは具体的なコードです。この過程で、抽象と具象の間に生じるギャップがコミュニケーションの大きな課題となります。

このギャップを適切に埋められないと、設計と実装の間で誤解や混乱が生じ、意図しない結果を招く可能性があります。

抽象と具象のギャップを埋めるためには、単なる会話やドキュメント、図だけではなく、実際のコードを共有することが効果的です。特に、インターフェースやその使い方をコード上で明確に示すことが有効でした。

これらをPull Request上でやりとりすることで、コードに落とし込んだ際の構造的な問題点を具体的に把握できるようになります。

その結果、抽象的な議論が具象化され、ギャップを最小限に抑えつつ、設計の方向性を共有・調整することが可能になりました。


廃止機能は削除する

地味な点ではありますが、不要なコードが残ったままになっていると、リファクタリングの効率に大きな影響を与えます。

リファクタリングは通常、振る舞いを変えずにコードを改善する作業ですが、不要なコードが残っていると、それを新しい構造に適合させるコストが発生します。それだけでなく、不要なコードが存在することで認知負荷が増大し、リファクタリング全体の効率が低下することもあります。

そのため、不要なコードを削除することは、間接的ながらリファクタリングの成功に大きく寄与します。これはリファクタリングの準備というより、日頃からコードを整備しておく習慣を持つことが重要、という話に近いかもしれません。


取り組みの結果

数百行に及ぶ長い関数を複数のサブモジュールに分割した結果、処理が独立したことで、コードの見通しが良くなり、メンテナンス性が大幅に向上しました。

各モジュールにインターフェースを定義したことで、特定の実装に依存せず仕様を確立でき、新しい要件への対応が容易になりました。これにより、モジュールの差し替えがスムーズに行え、柔軟性が向上しています。

また、モジュール単位でのテスト環境を整備したことで、リファクタリングや機能追加時のシステム全体への影響を最小限に抑えられるようになり、安心して開発を進められる基盤が整いました。

加えて、カナリアリリースやストラングラーパターンといった手法の適用が可能になり、大規模な変更を段階的に本番環境へ反映できるようになりました。

これらの改善により、迅速かつ安全なリリースサイクルが期待できます。


おわりに

今回のアーキテクチャ改良では、依存関係の整理とモジュールの分割が鍵となることが明らかになりました。特に重要だったのは、疎結合なモジュール設計と依存方向を意識した設計です。これにより、変更時の他モジュールへの影響を最小限に抑え、効率的にシステムを改善できました。

また、インクリメンタルな改善戦略も大切です。小さな変更を積み重ねることで、大きなリスクを回避しつつ、システム全体を持続可能な形へと段階的に進化させることが可能です。

さらに、効果的なコミュニケーション手段も重要です。チーム全体が同じ方向に進むためには、設計思想や依存の方向性に関する共通認識を持ち、適切なコミュニケーションを行うことが不可欠です。

アーキテクチャは一度作って終わりではありません。構造の価値を維持するためには、継続的なケアが必要です。

そのためには、コードの改善を続けるだけでなく、開発者全体の構造への意識を高めることや、スキルアップの促進、さらには組織構造やプロセスの整備も欠かせません。

私たちはアーキテクチャそのものだけでなく、プロジェクトを構成するあらゆる要素に目を向け、持続可能なシステムを実現する責任があります。

ソフトウェア・アーキテクチャに銀の弾丸は存在しません。目的を明確にし、最適な構造を追求することこそが、我々の存在価値であり、責務ではないでしょうか。

今回の事例が、読者の皆様の参考になれば幸いです。


裏話

言葉を増やさない

実は、セッション中にもあった「言葉を増やさない」というポリシーは、このセッション全体でも意識されています。たとえば、「モジュラーモノリス」や、「依存性の逆転」などの言葉も、スライドの初期バージョンでは普通に使われていたのですが、最終バージョンでは全て削除しました。

DDDについて

概念自体は難解ではありますが、DDDに習熟し設計をリードできるメンバーがいる場合、少人数のプロジェクトで採用するには悪い選択肢ではありません。

DDDは適切な秩序を分かりやすい概念と共に提供してくれる素晴らしいツールです。ただし、そのボリュームゆえに、取り入れる概念は取捨選択が必要になるかもしれません。

適切なアーキテクチャを選択し続けるということ

たとえば、マイクロサービスアーキテクチャは一時期非常に流行しましたが、運用の難しさが明らかになり、最近では敬遠されることも少なくありません。その一方で、モジュラーモノリスが注目を集めています。

とはいえ、マイクロサービスアーキテクチャも適切に運用すれば非常に有効な選択肢です。重要なのは、さまざまなアーキテクチャを理解したうえで、課題とフェーズに最も適したものを選び、あるいは新たに設計することです。

そのために、我々は学び続ける責任があります。


アーキテクチャConference 2024について

最後にイベント自体にも触れておきます。

オンライン・オフライン含め非常に多くの人にお集まり頂き、通路では人が通れないほどの盛況でした。
セッションもソフトウェアアーキテクチャをはじめ、クラウドアーキテクチャやデータアーキテクチャに至るまで、様々な発表があり、どれも興味深い内容で、とても有意義な一日でした。

オープンロジブースの様子
CTO、VPoE、ブース運営メンバー

次回も非常に楽しみですね!


オープンロジについて

オープンロジは、「物流をもっと簡単、シンプルに」というコンセプトで「物流版クラウドサービス」を目指し、様々な課題を解決しています。

物流版クラウドサービスとは?

我々ソフトウェア・エンジニアは、日々の開発や運用においてAWSやGoogle Cloudのようなクラウドサービスの恩恵を受けています。

例えば、アプリケーションのインフラを構築する際、クラウドプラットフォームを活用することで、サーバーの調達や設定といった煩雑な作業を大幅に省略できます。

さらに、スケーラビリティの高い環境を簡単に実現できるため、アクセス集中時でも安定したサービス提供が可能です。また、ストレージやデータベース、機械学習APIなど、多彩なサービスを統合しやすい点も魅力的です。

これらの機能を活用することで、より迅速かつ柔軟な開発が可能となり、クライアントやユーザーに価値を提供し続けることができています。

物流でも、倉庫がクラウドサービスのように柔軟でスケーラブルな仕組みを持てれば、大きなメリットが生まれるでしょう。

例えば、需要が急増する繁忙期に追加の倉庫を素早く確保できる仕組みがあれば、在庫切れや配送遅延を防ぐことができます。

一方、閑散期には倉庫リソースを最適化してコスト削減につなげることが可能です。
さらに、倉庫内の在庫状況をリアルタイムで可視化できれば、正確な在庫管理や即時の意思決定が可能になり、サプライチェーン全体の効率が向上します。

オープンロジは、このような世界を目指し、全力で成長しています!


We are hiring!

オープンロジは比較的歴史も長く、技術的に難しい課題や、ビジネス上の課題が無数に存在します。

昨今話題となっている物流における社会問題解決の一助になるべく、職種を問わず全力で奔走しています。

比較的抽象度の高い領域で仕事をすることも多く、俯瞰した視点を得たいITエンジニアにとって成長できるフィールドとなるのではないでしょうか。

もしこの記事を読んでオープンロジに興味を持って下さった方は是非、カジュアルに話してみませんか?


この記事が参加している募集