[codex] 과목 조회 캐시와 warm-up 추가#21
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces caching capabilities to the subject catalog using Caffeine as the default in-process cache and Redis as an optional shared cache, along with a mechanism to persist and restore Caffeine cache snapshots to disk. It also implements event-driven cache eviction triggered by administrative updates. The review feedback suggests improving the resilience of the cache snapshot restoration by handling deserialization errors per entry, and ensuring that temporary files are cleaned up if the file move operation fails.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| int restored = 0; | ||
| for (SubjectCacheSnapshotEntry entry : snapshot.entries()) { | ||
| if (!SubjectCacheNames.ALL.contains(entry.cacheName())) { | ||
| continue; | ||
| } | ||
|
|
||
| Optional<com.github.benmanes.caffeine.cache.Cache<Object, Object>> nativeCache = | ||
| nativeCache(entry.cacheName()); | ||
| if (nativeCache.isEmpty()) { | ||
| continue; | ||
| } | ||
|
|
||
| Object key = deserializeKey(entry.cacheName(), entry.key()); | ||
| Object value = deserializeValue(entry.cacheName(), entry.value()); | ||
| nativeCache.get().put(key, value); | ||
| restored += 1; | ||
| } |
There was a problem hiding this comment.
If a single cache entry fails to deserialize (e.g., due to a class refactoring or DTO schema change), the entire snapshot restoration process is aborted. Wrapping the deserialization and cache insertion inside the loop with a try-catch block will make the restoration process more resilient, allowing other unaffected cache entries to be successfully restored.
int restored = 0;
for (SubjectCacheSnapshotEntry entry : snapshot.entries()) {
if (!SubjectCacheNames.ALL.contains(entry.cacheName())) {
continue;
}
Optional<com.github.benmanes.caffeine.cache.Cache<Object, Object>> nativeCache =
nativeCache(entry.cacheName());
if (nativeCache.isEmpty()) {
continue;
}
try {
Object key = deserializeKey(entry.cacheName(), entry.key());
Object value = deserializeValue(entry.cacheName(), entry.value());
nativeCache.get().put(key, value);
restored += 1;
} catch (Exception exception) {
log.warn("Failed to deserialize cache entry for cache: {}", entry.cacheName(), exception);
}
}| try { | ||
| Files.move(tempPath, snapshotPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); | ||
| } catch (AtomicMoveNotSupportedException exception) { | ||
| Files.move(tempPath, snapshotPath, StandardCopyOption.REPLACE_EXISTING); | ||
| } |
There was a problem hiding this comment.
If the file move operation fails (for example, due to permission issues or other I/O errors), the temporary .tmp file will be left behind on the disk. It is best practice to clean up the temporary file in a finally block to prevent cluttering the file system.
try {
Files.move(tempPath, snapshotPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(tempPath, snapshotPath, StandardCopyOption.REPLACE_EXISTING);
} finally {
try {
Files.deleteIfExists(tempPath);
} catch (IOException ignored) {
}
}2fde898 to
8833bd2
Compare
8833bd2 to
55c56e8
Compare
55c56e8 to
4dfbfde
Compare
요약
배경
과목 데이터는 학기 단위 또는 관리자 import 시점에만 바뀌는 read-heavy 데이터입니다. 반면 학생들은 같은 과목 수, 학과/학년 목록, 검색/필터 조회를 반복해서 호출합니다. 그래서 외부 캐시 인프라를 추가하지 않고, 현재 단일 인스턴스 운영에 맞는 Caffeine 로컬 캐시를 기본으로 적용했습니다.
운영 방식
subject.cache.expire-after-write로 조정합니다. 기본값은10m입니다.subject.cache.maximum-size로 조정합니다. 기본값은1000입니다.subject.cache.warm-up.enabled로 켜고 끌 수 있으며 기본값은true입니다.subject.cache.warm-up.concurrency로 제한합니다. 기본값은4입니다.subject.cache.warm-up.page-size로 조정합니다. 기본값은20이며, 필터 캐시에 저장 가능한 최대 page size는100입니다.size > 100인 필터 조회 결과는 과도한 메모리 점유를 피하기 위해 캐시에 저장하지 않습니다.검증
./gradlew test --tests 'inu.timetable.service.SubjectQueryServiceCacheTest' --tests 'inu.timetable.service.SubjectCacheWarmupServiceTest' --tests 'inu.timetable.config.CacheConfigTest'./gradlew test bootJargit diff --check