Caffeine.newBuilder()
        .removalListener((key, value, cause) -> {
            if (cause.wasEvicted()) System.out.printf("key=%s, value=%s", key, value);
        })
        .expireAfterWrite(60, TimeUnit.SECONDS)
        .build();

위처럼 caffine cache에 expire listener를 걸고 60초 expire after write 설정을 했다면, 키를 put 한 뒤 60초 뒤에 expire listener가 실행되는 것을 기대할 것이다

하지만 실제로 써보면 removal listener가 100% 동작하지는 않는다.

 

By default, Caffeine does not perform cleanup and evict values "automatically" or instantly after a value expires. Instead, it performs small amounts of maintenance work after write operations or occasionally after read operations if writes are rare.

- https://github.com/ben-manes/caffeine/wiki/Cleanup

그 이유는, 기본적으로는 caffeine이 주기적으로 cleanup을 하는게 아니고, cleanup이 lazy하게 읽기 또는 쓰기 시점 이후에 동작하기 때문이다.

설정한 expire 시간에 listener가 호출되도록 하려면, (1) 직접 쓰레드를 생성해 적절히 Cache.cleanUp()을 호출하거나, (2) 시스템 기본 스케줄러를 사용할 수 있다 (jdk9 이상)

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .scheduler(Scheduler.systemScheduler())
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

주의할 점은 스케줄링이 best-effort라서 expire 된 엔트리가 (즉시) 삭제된다는 것은 보장되지 않는다는 것이다. 재수가 없으면 한참 걸린다.

 

Reference-based 엔트리를 쓴다면(weakKeys, weakValues, ...) Cleaner를 사용해야 한다고 한다. 이건 써보지 않아서 뭔진 모르겠다. 위에 Caffeine 위키를 참고하자.

 

이 동작을 찾아본 이유는, 여러개의 Caffeine cache를 줄줄이 expire + removal listener로 엮은 로직을 만들어봤는데 트리거가 잘 안되어서 찾아봤다.

시간에 민감한 로직은 이런식으로 짜면 안될 것 같다

반응형