실행 방법입니다. 외부 jar 는 필요 없습니다.
cd java-src\extension\18_cache
C:\project\jdk-21.0.8\bin\javac -encoding UTF-8 *.java
C:\project\jdk-21.0.8\bin\java -Dstdout.encoding=UTF-8 Main # 약 1초Cache<K,V> — TTL·스탬피드 방지·LRU 근사// Cache.get: computeIfAbsent 로 같은 키의 동시 미스를 1회로 합친다
Entry<V>[] created = new Entry[1];
Entry<V> entry = store.compute(key, (k, old) -> {
if (old != null && old.expireAtMillis > now) {
return old; // 그 사이 다른 스레드가 이미 채웠음
}
V value = loader.apply(k);
created[0] = new Entry<>(value, now + ttl.toMillis(), now);
return created[0];
});store 는 ConcurrentHashMap 이고, compute 는 같은 키에 대해 원자적으로 실행되므로 여러 스레드가 동시에 미스해도 loader.apply(k) 는 한 번만 불립니다. Clock 을 주입받기 때문에, 테스트에서는 MutableClock 으로 시간을 앞당겨 TTL 만료를 즉시 재현할 수 있습니다.
== 1. 느린 조회 캐싱 ==
첫 호출 291ms, 둘째 호출 1ms (캐시 적중) ← 첫 호출만 200ms sleep 을 탄다
hit=1 miss=1 size=1 hitRatio=50.0%
== 2. TTL 만료 ==
loader 호출 횟수: 2 (기대값 2) ← 적중 1회는 재호출 없음
hit=1 miss=2 size=1 hitRatio=33.3%
== 3. 캐시 스탬피드 방지 ==
동시 미스 10개, loader 실행 1회 (기대값 1) ← 10 스레드가 동시에 미스해도 1회
== 4. 쓰기 후 invalidate ==
수정 전: old-name
수정 후: new-name ← invalidate 뒤 새 값으로 재계산
== 5. 크기 제한 초과 시 제거 ==
최대 크기 3인데 5개 입력 -> 현재 크기: 3 ← 오래된 항목부터 제거
hit=0 miss=5 size=3 hitRatio=0.0%예제 2 의 TTL 만료는 실제로 5분을 기다리지 않고, MutableClock.advance(Duration.ofMinutes(6)) 로 시계를 앞당겨 즉시 재현했습니다. 예제 3 은 CountDownLatch 로 10개 스레드를 동시에 출발시켜 진짜 동시 접근 상황을 만들었습니다.