SpringCache による処理高速化の実現

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 の学習や導入を、処理パフォーマンスのせいで断念するのを避けられれば光栄です。