yu nkt’s blog

nkty blog

I'm an enterprise software and system architecture. This site dedicates sharing knowledge and know-how about system architecture with me and readers.

アプリアーキテクチャにおけるLazy loading

背景

昨年、Google Chromeを始め、Lazy loading(レイジーローディング)と呼ばれる機能がブラウザ標準として実装され、フロントエンドでは少し話題になったようです。 このLazy loading、実はアプリケーションアーキテクチャでも、"DDDにおける大きな集約の取り扱い"において、利用価値があるものだと思われます。

この記事では、その説明に入る前に、Lazy loadingとは?という話を書いていこうと思います。

Lazy loadingの概要

Lazy loadingは、日本語では遅延読み込みと訳されるもので、以下のようなテクニックのことです。

  • DBにある複数のデータを塊として必要とする
  • ただし、その塊の中の一部のデータは実際には使われない可能性もある
  • そこで、その塊を取得する処理の時点では、取ったことにしておく
  • 実際に塊の中のデータを利用するときに、改めてDBからそのデータを取得する

このテクニックは、Martin Fowler氏のPofEEA (Patterns of Enterprise Application Architecture)にも載っています。

martinfowler.com

フロントエンドにおけるLazy loading

おそらく2020年現在、日本語でLazy loadingについて調べると、フロントエンドの記事が多く出てくると思います。 それは、ちょうど近年、多くのブラウザが、Lazy loadingを標準で実装し始めたためです。

Google Chromeは、'19/7/30のGoogle Chrome76で実装。

web.dev

Firefoxは、'20/4/7のFirefox75で実装。

hacks.mozilla.org

フロントエンドにおいては、画像の遅延読み込み、という文脈でlazy loadingが登場します。 例えば、画像が大量にあるページがあるとします。そして、そのページを、スマホユーザが開いたとします。 ページを開いた瞬間、スマホの画面に表示できる画像は、物理的に、スマホの画面に収まる分だけです。

この時、スマホの画面内で見えていない、ページ下部の画像は、ページを開いた直後でもスマホにダウンロード済みにしておく必要があるでしょうか? そんなことはありません。ユーザが閲覧しようのない(そして実際に見ずにページを閉じるかもしれない)画像を、Webサーバからダウンロードしてくるなんて無駄なはずです。

ただし、Webサーバからページの構成に必要なデータを取得し終えたことにしておかないと、スマホのローディング中マークは常に出続けます。また、データのロード中は、ユーザのブラウザ操作に支障があるかもしれません。

そこで、Lazy loadingは、画像を抜いた状態で一旦ページを読み込んだことにして、ユーザが必要としたときに改めて画像をダウンロードして表示するのです。 これにより、以下のような、ユーザにもサーバ側にもwin-winの効果があります。

  • ネットワーク帯域やサーバ負荷を抑えられる (モバイル端末にとってはデータ通信量を抑えられる)
  • ページを早く取り込めて、ユーザがブラウザ操作を待たされる時間を減らせる

従来より、JavaScriptを使って、Lazy loadingを利用することは出来ていましたが、ブラウザが標準で実装したことによって、HTMLタグのみで、利用できるようになりました。

<img loading="lazy" src="img/example.jpg" width="765" height="574" />

アプリケーションアーキテクチャにおけるLazy loading

Lazy loadingは、アプリケーションアーキテクチャにおいても利用されます。 例えば、DDDにおいて、集約を取得する際に、Lazy loadingを使うことがありえます。

ファクトリからドメインオブジェクトを作成する際に、まずはデータが全て揃っていないドメインオブジェクトを用意します。 その後、実際に中身のデータが利用されるときに改めてDBからデータを取得します。 なぜそんなことをするかというと、ドメインとしての設計上(ビジネス上の実体を反映させた場合に)、あるドメインオブジェクトには、A, B, Cというデータが内包されるとするのが妥当かもしれないが、そのドメインオブジェクトの操作によっては、AとBしか使わない場合がある、というケースがあり得るからです。

例で考えてみましょう。自動車というドメインオブジェクト(エンティティであり集約ルートとします)があったとします。 自動車にはエンジンが1つ、タイヤが4つあります(他にも多量のパーツがありますが無視します)。 他にも車種という情報もあるでしょう。

車検サービスに対しては、この自動車ドメインオブジェクトが必要です。自動車全体に対して、検査をするためです。

しかし、車検サービスの他に、タイヤ交換サービスがあったとします。このサービスにも、自動車を持っていくでしょう。 おそらく、タイヤ交換サービスには、タイヤと車種の情報が必要です。ただ、この時、車にはエンジンもあります。 タイヤ交換サービスには、エンジンは必要ありません。 これをプログラムとして考えたとき、DBから自動車ドメインオブジェクトを生成するために、エンジンのデータまでDBから取ってくる必要があったかどうか、という話になります。 Lazy loadingを用いることで、まず中身のない自動車ドメインオブジェクトを作成した後に、タイヤ交換サービスが自動車ドメインオブジェクトを利用する際に、改めてタイヤと車種のデータをDBから取得します。

そもそもオブジェクト生成時に、必要なデータだけDBから取ればいいのでは?

以下のような疑問を持つ人はいるかも知れません。

  • なぜ、自動車というドメインオブジェクトをタイヤ交換サービスに持っていっているの?
  • タイヤと車種の情報だけを持っていけばいいのでは?
  • そうしたら、別にLazy loadingなんて使わなくていいんじゃない?

以下は、Martin Fowler氏のLazy loadingの説明の出だしの文章ですが、上記の疑問は、以下の文章の「関心のあるオブジェクトだけでなく、それに関連するオブジェクトも同時に読み込むように設計してあると便利」とはどういうこと?という疑問とほぼ同義だと思います。

For loading data from a database into memory it's handy to design things so that as you load an object of interest you also load the objects that are related to it. This makes loading easier on the developer using the object, who otherwise has to load all the objects he needs explicitly.

データベースからデータをメモリ上にロードするとき、関心のあるオブジェクトだけでなく、それに関連するオブジェクトも同時に読み込むように設計してあると便利である。開発者にとってオブジェクトのロードが楽になり、必要なすべてのオブジェクトを明示的にロードする必要がなくなる。

これは、ドメインの設計に依存するポイントです。

自分がドメインエキスパートだと思って考えてみてください。 あなたは、冬の前にディーラーへ、スタッドレスタイヤに交換してもらいにいくとき、タイヤと車種の情報だけをディーラーに持っていきますか?

Yesはありえます。タイヤだけもらってきて、自宅のガレージで取り付ける、という人です。 この場合、その人とディーラーとの間は、「古いタイヤと車種の情報を持ってきてくれたら、新しいタイヤを出す」という"契約"が存在しています(c.f. 契約による設計)。"あなた"自身は、タイヤを外して取り付ける、という行いの責務を持っていると定義されている状態です。

私は大抵Noで、車自体を持っていきますので、前述のような設計を前提に書きました。 この時、私とディーラーの間では、「自動車を持ってきてくれたら、後は対応する」という"契約"が存在しています。 私は、タイヤ交換に何が必要なのか知らずに済みます。言い換えると、私の興味がないことを知らなくて済むのです。 ディーラーのタイヤ交換のやり方を変えようが、それによって私が何かしないといけないわけでなく、ディーラーの中で閉じるのです(つまり、依存していない、ということ)。

一般に自動車の日頃のメンテナンスは、購入者の責務と言えます。その責務の中に、タイヤを外して取り付ける、が含まれるかどうか、というドメイン判断次第です。

つまり、Lazy loadingが必要となるドメインオブジェクトかどうかは、DDDのドメイン設計に依存します。

データが小さいならLazy loadingを使うまでもない

もう一つ、補足したいことがあります。 エンジンのデータは、Lazy loadingをしないといけないほど、大規模ですか?という問いです。

別の記事で書きますが、Lazy loadingをする場合、多少追加のコーディングが必要です。 スレッドセーフを意識した効率的なLazy loadingの実装を考えると、意外と面倒(というか初見では、え?となりかねない)コードだったりします。

メンテしないといけないコードを増やしてまで、Lazy loadingをすべきか、考えるべきです。 もし自動車ドメインオブジェクトのエンジンが、エンジンの型番だけしか持たなくていいなら、Lazy loadingを使わず常にDBからエンジンのデータ(型番のデータ)を取り出せばいいと思います。

ただ、もし自動車会社の自動車設計システムを作っていた場合、エンジンの中身まで全てデータとして持つ必要があるかもしれません。 今調べたら、エンジンは、約1万点の部品で構成されているそうです。もしかしたら値オブジェクトで考えて種類数を見るべきかもしれませんが、それでも数千だそうです。 つまり、自動車ドメインオブジェクトをファクトリから生成するたびに、数千のデータを取り出し、しかもそれが使われないかもしれない、となれば、考えものです。 その際の手の"一つ"として、Lazy loadingは使えます。

実は、こういった問題は、DDDで開発していると、頻繁に起きます。 それ故にかもしれませんが、この問題を解決するテクニックは、Lazy loadingだけではありません。むしろ、Lazy loadingは後回しでいいとさえ思っています。 他のテクニックや、それらと比較したLazy loadingの使い時は、気の向いたときに別の記事に書きます。

次回予告

文中に度々書いたとおり、本当は、まだまだ書きたいことがあります。例えば、以下のような話です。

  • Lazy loadingの実装方法
  • Lazy loadingと関連するN+1問題
  • DDDにおける大きな集約の扱い方

私自身は夏休みに入って、時間があるので、近日中に頭の整理をして書こうと思います。 今回の内容でも、書き始めたら結構興味深い点が多いと感じたので、関連記事があと3つくらいかけるかもと思っています。