DDD とパフォーマンス
DDD やモデル駆動で設計・開発をしていると、集約ルートが大きくなり、パフォーマンス面で課題が生じることがあると思います。
CQRS
などで ReadModel
を定義するなどの解決策はあると思いますが、今回はキャッシュを使った解決策を検討してみようと思います。
SpringCache
Java で API などバックエンドシステムを開発する際のデファクトスタンダードなフレームワーク SpringBoot
ですが、SpringCache
というフレームワークも用意されています。
ありがたいことに比較的簡単に導入できるので、気軽に検証してみるのも良いかと思います。
Sapmle API
build.gradle
まずは build.gradle
をこんな感じで設定します。(いきなりgradle
前提ですみません...)
~~~
dependencies {
implementation `org.springframework.boot:spring-boot-starter-cache`
~~~
}
~~~
これで SpringCache
フレームワークを導入できます。そのほかのフレームワークは必要に応じて追加します。
SpringCache の有効化
忘れないように @EnabledCaching
アノテーションを付与しておきます。
@SpringBootApplication
@EnabledCaching
public class SampleApiApplication {
~~
~~
}
Cacheable なメソッド
SpringCache
のメインとなる機能はメソッドに対してキャッシュを効かすことができる点と思います。
例えば下記のようなイメージです。
@Service
public class EmployeeQueryService {
@Autowired
EmployeeRepository repository;
@Cacheable("listEmployee")
public List<Employee> listEmployee() {
return repository.findAll();
}
}
キャッシュを効かせたいメソッド listEmployee()
にアノテーション @Cacheable
を付与するだけで OK です。
アノテーションの引数 "listEmployee"
はキャッシュを識別するためのラベルです。(詳細は後述します)
何が起きるのか?
上記の状態で EmployeeQueryService::listEmployee
を呼び出すと、SpringCache
が "listEmployee"
のラベルが付いたキャッシュを探します。
キャッシュが存在しなければ通常通りメソッドが実行され、その結果が return される前に、返却する結果を "listEmployee"
ラベルを付与してキャッシュとして保存します。
そのため、初回のメソッドコール時は通常通りメソッドが実行され、2回目以降はキャッシュされた結果が返却されます。
ちなみに、上記のようなデフォルト状態で SpringCache
を利用した場合、キャッシュはプロセス内のメモリに保存されます。
キャッシュの有効期限
アノテーションを付与するだけでキャッシュを導入することができましたが、このままではいくつか問題があります。特に「2回目以降はキャッシュされた結果が返却」されてしまうので、永続化データ自体に変更があった場合はそれを取得することができないです。
そこでキャッシュに有効期限を設定し、この問題を解決しようと思います。
CacheManager
SpringCache
に関する諸々の設定を施せるオブジェクトとして CacheManager
が用意されています。
CacheManager
自体はインタフェースなのですが、既にいくつかの実装クラスも用意されています。
- SimpleCacheManager
- ConcurrentMapCacheManager
- NoOpCacheManger
などです。
キャッシュの有効期限設定機能はない...
残念ながらデフォルトで用意されている CacheManager
の実装クラスには、有効期限を設定するような機能はありませんでした。
そこで 1 つの案として、定期的にキャッシュを削除することで、"有効期限を持っている風にする" ということはできるかと思います。
@Service
public class ClearCacheService {
@Autowired
CacheManager cacheManager;
@Scheduled(fixedRate = 10 * 1000)
public void clear() {
cacheManager.getCacheNames().forEach(
name -> cacheManager.getCache(name).clear()
);
}
}
上記は CacheManager
を利用してキャッシュを全削除するメソッド clear()
を用意し、それを @Scheduled
により定期実行させる例です。
@Scheduled
の引数は ms
指定のため、上記の場合は 10 秒間隔でキャッシュが全削除されるようになります。
(補足) メインのクラスに@EnableScheduling
の付与をお忘れなく。
Caffeine を利用したローカルキャッシュ
しかし、一定間隔でキャッシュを削除するのはどうにも微妙だなぁと思い調査したところ、Caffeine
というローカルキャッシュのライブラリを見つけました。そして、SpringCache
にはすでに CaffeineCacheManager
の実装クラスが用意されていたので、簡単に導入できそうです。
build.gradle
implementation 'com.github.ben-manes.caffeine:caffeine'
を追加しましょう。
CaffeineCacheManager を Bean Conatiner に登録
CacheManager
の実装として CaffeineCacheManager
が利用されるように @Bean
などで登録します。
そして、その際に CaffeineCacheManager
に対して キャッシュ有効期限を設定することができます!
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
CaffeineSpec spec = CaffeineSpec.parse("expireAfterWrite=10s");
cacheManager.setCaffeineSpec(spec);
return cacheManager;
}
上記の CaffeineSpec.parse("expireAfterWrite=10s")
が有効期限の設定です。
expireAfterWrite
パラメータはキャッシュが作成されてからキャッシュを保持する期間です。まさに有効期限ですね。
(補足) Caffeine のそのほかのパラメーター
expireAfterWrite
があれば問題なさそうですが、他にもいくつかのパラメータがありました。
- expireAfterAccess
- 最後にキャッシュにアクセスしてからキャッシュを保持する期間。永遠にキャッシュにアクセスしてたらキャッシュはクリアされないですね。
- refreshAfterWrite
- 最後にキャッシュが更新されてからキャッシュを保持する期間。キャッシュの更新自体は割愛します。
まとめ
SpringCache
を利用することで比較的スムーズ、かつ、簡単にキャッシュ機能を導入することができました。
今回紹介したのはローカルキャッシュのため懸念点はいくつかありますが、「とりあえず高速化しないと!」いけない場面では有効に機能するかと思います。
また、基本的には Caffeine
ライブラリをセットで利用することで、キャッシュの有効期限を設定可能にした方が良いでしょう。
CacheManeger
には Redis
向けの実装など、まだまだたくさんの実装クラスがあります。
それらを活用すれば、キャッシュ場所をメモリではなくRedis
などの外部ストレージにすることも可能でしょう。
皆さんが DDD
の学習や導入を、処理パフォーマンスのせいで断念するのを避けられれば光栄です。