Skip to content

[codex] 과목 조회 캐시와 warm-up 추가#21

Draft
coldmans wants to merge 1 commit into
mainfrom
codex/add-subject-cache
Draft

[codex] 과목 조회 캐시와 warm-up 추가#21
coldmans wants to merge 1 commit into
mainfrom
codex/add-subject-cache

Conversation

@coldmans

@coldmans coldmans commented Jun 12, 2026

Copy link
Copy Markdown
Owner

요약

  • 과목 조회 API에 Caffeine 기반 로컬 캐시를 적용했습니다.
  • 과목 수, 학과 목록, 학년 목록, 과목명 검색, 교수명 검색, 필터 조회를 캐시 대상 조회 서비스로 분리했습니다.
  • 애플리케이션 시작 후 과목 수, 학과 목록, 기본 필터, 학년별 첫 페이지를 제한된 동시성으로 warm-up 합니다.
  • 관리자 생성/수정/삭제, 수동 import, PDF/Excel/공식 과목 import 이후에는 트랜잭션 커밋 후 과목 조회 캐시를 무효화합니다.

배경

과목 데이터는 학기 단위 또는 관리자 import 시점에만 바뀌는 read-heavy 데이터입니다. 반면 학생들은 같은 과목 수, 학과/학년 목록, 검색/필터 조회를 반복해서 호출합니다. 그래서 외부 캐시 인프라를 추가하지 않고, 현재 단일 인스턴스 운영에 맞는 Caffeine 로컬 캐시를 기본으로 적용했습니다.

운영 방식

  • 기본 캐시 TTL은 subject.cache.expire-after-write로 조정합니다. 기본값은 10m입니다.
  • 캐시 크기는 subject.cache.maximum-size로 조정합니다. 기본값은 1000입니다.
  • warm-up은 subject.cache.warm-up.enabled로 켜고 끌 수 있으며 기본값은 true입니다.
  • warm-up 동시성은 subject.cache.warm-up.concurrency로 제한합니다. 기본값은 4입니다.
  • warm-up page size는 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 bootJar
  • git diff --check

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +138 to +154
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);
                }
            }

Comment on lines +228 to +232
try {
Files.move(tempPath, snapshotPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(tempPath, snapshotPath, StandardCopyOption.REPLACE_EXISTING);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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) {
            }
        }

@coldmans coldmans changed the title [codex] Add subject query caching [codex] 과목 조회 캐시 추가 Jun 12, 2026
@coldmans coldmans force-pushed the codex/add-subject-cache branch from 2fde898 to 8833bd2 Compare June 12, 2026 11:55
@coldmans coldmans changed the title [codex] 과목 조회 캐시 추가 [codex] 과목 조회 캐시와 warm-up 추가 Jun 12, 2026
@coldmans coldmans force-pushed the codex/add-subject-cache branch from 8833bd2 to 55c56e8 Compare June 12, 2026 12:03
@coldmans coldmans force-pushed the codex/add-subject-cache branch from 55c56e8 to 4dfbfde Compare June 12, 2026 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant