Skip to content

refactor: ThunderingHerdCaching의 만료 임박 캐시 갱신 정책 수정#802

Merged
Hexeong merged 2 commits into
developfrom
refactor/801-change-cache-policy
Jun 27, 2026
Merged

refactor: ThunderingHerdCaching의 만료 임박 캐시 갱신 정책 수정#802
Hexeong merged 2 commits into
developfrom
refactor/801-change-cache-policy

Conversation

@Hexeong

@Hexeong Hexeong commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

관련 이슈

작업 내용

  • ThunderingHerdCaching의 만료 임박 캐시 갱신 정책을 수정했습니다.

    • 기존에는 갱신락을 획득해도 Redis TTL만 연장하고 기존 캐시값을 그대로 반환했습니다.
    • 변경 후에는 갱신을 맡은 요청이 실제 메서드를 다시 실행해 최신 값을 계산하고 Redis 캐시에 저장합니다.
    • 다른 요청은 기존 캐시값을 반환하거나, refresh 중 캐시가 만료된 경우 생성락 대기 흐름을 통해 갱신된 값을 읽습니다.
  • refresh 중 캐시 key가 만료되어도 중복 계산이 발생하지 않도록 보강했습니다.

    • Codex 리뷰 반영: refresh와 create가 같은 생성락을 공유하도록 변경했습니다.
    • refresh 완료 후 CREATE_CHANNEL로 publish해, 캐시 miss로 진입해 대기 중인 요청이 갱신된 캐시값을 읽도록 했습니다.
  • 갱신 실패 시 fallback 기준을 정리했습니다.

    • 일반 RuntimeException이 발생하면 기존 캐시값을 반환하되 TTL은 연장하지 않도록 했습니다.
    • CustomException은 기존 캐시값으로 fallback하지 않고 그대로 전파하도록 분리했습니다.
  • Redis TTL 경계값 처리를 보강했습니다.

    • getExpire() 결과가 null, -1, -2이거나 기본 TTL/percent 값이 비정상인 경우 만료 임박으로 판단하지 않도록 방어 로직을 추가했습니다.
    • CodeRabbit 리뷰 반영: percent <= 0 또는 percent > 100인 경우도 비정상 값으로 처리합니다.
  • ThunderingHerdCachingAspectTest를 추가 및 보강했습니다.

    • 만료 임박 캐시 갱신 시 값 재계산
    • 갱신락 획득 실패 시 기존 캐시값 반환
    • 갱신 중 일반 예외 발생 시 기존 캐시값 반환 및 TTL 미연장
    • 갱신 중 CustomException 발생 시 예외 전파
    • 만료 시간이 없는 캐시는 만료 임박으로 판단하지 않는 케이스
    • 유효하지 않은 refresh percent는 만료 임박으로 판단하지 않는 케이스
    • refresh 중 기존 캐시가 만료되어도 생성락으로 중복 계산을 막는 케이스
    • CodeRabbit 리뷰 반영: 성공 계산 횟수와 실제 메서드 진입 횟수를 분리해 실패 경로의 refresh 시도를 검증합니다.

특이 사항

  • @ThunderingHerdCaching Annotation의 입력 인터페이스는 변경하지 않았습니다.
  • 기존 호출부 변경 없이 Aspect 내부 정책만 수정했습니다.
  • 전체 테스트 종료 시 기존 비동기 스케줄러가 Redis 커넥션 종료 중 접근하는 로그가 출력될 수 있으나, 테스트 결과는 성공했습니다.

리뷰 요구사항 (선택)

  • 만료 임박 캐시에서 한 요청만 최신 값을 재계산하고, 나머지 요청은 기존 캐시값을 반환하거나 생성락 대기 흐름을 타는 정책이 적절한지 확인 부탁드립니다.
  • refresh와 create가 생성락을 공유하는 방식이 Thundering Herd 방어 흐름에 적절한지 확인 부탁드립니다.
  • CustomException을 fallback 대상에서 제외하고 도메인 예외로 전파하는 방식이 기존 예외 처리 흐름과 잘 맞는지 확인 부탁드립니다.
  • Redis TTL이 없거나 key가 사라진 경계 상황, 또는 percent 설정값이 비정상인 상황을 만료 임박으로 보지 않는 방어 로직이 충분한지 확인 부탁드립니다.

검증:

  • bash ./gradlew test --tests com.example.solidconnection.cache.ThunderingHerdCachingAspectTest
  • bash ./gradlew test --tests com.example.solidconnection.cache.ThunderingHerdCachingAspectTest --tests com.example.solidconnection.concurrency.ThunderingHerdTest --tests com.example.solidconnection.university.service.GeneralUnivApplyInfoRecommendServiceTest --tests com.example.solidconnection.university.service.UnivApplyInfoRecommendServiceTest --tests com.example.solidconnection.university.service.UnivApplyInfoQueryServiceTest
  • bash ./gradlew test

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

  1. 캐시 만료 임박 판정 강화
    • isCacheExpiringSoon에서 defaultTtl, leftTtl, percent의 비정상 값은 바로 false로 처리합니다.
  2. 캐시 갱신 경로 변경
    • 만료 임박 캐시 히트에서 refreshCache가 새 값을 계산해 cacheManager.put으로 갱신하고 반환합니다.
    • 갱신 중 RuntimeException은 기존 값을 반환하고, CustomException은 전파합니다.
  3. 테스트 추가
    • 갱신, 실패, 예외, TTL 없음 시나리오를 검증하는 테스트를 추가했습니다.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 주요 변경인 만료 임박 캐시 갱신 정책 수정과 잘 맞는 구체적인 제목입니다.
Linked Issues check ✅ Passed refresh 재계산, 실패 시 fallback, TTL 경계값 방어, 테스트 보강이 이슈 #801 요구사항과 부합합니다.
Out of Scope Changes check ✅ Passed Aspect, 유틸, 테스트 범위 내 변경만 보이며 별도 목적과 무관한 변경은 보이지 않습니다.
Description check ✅ Passed 필수 섹션(관련 이슈, 작업 내용, 특이 사항, 리뷰 요구사항)을 모두 포함하고 있어 템플릿을 충분히 따릅니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/801-change-cache-policy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7aee8df295

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +97 to +98
Object result = proceedJoinPoint(joinPoint);
cacheManager.put(key, result, ttl);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the key alive while refresh recomputes

When a key is already in the refresh window and the underlying method takes longer than the small TTL that remains, this path does not extend or otherwise reserve the cached key until after proceedJoinPoint returns. The key can expire while the refresh lock is still held, so later requests enter the cache-miss/create-lock path and execute the same expensive method concurrently; whichever cacheManager.put finishes last then overwrites the cache, which reintroduces the thundering-herd/racing-write behavior this aspect is meant to prevent. The previous getAndExpire extended the TTL before returning, so consider extending/marking the key before recomputation or coordinating refresh and create locks.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

refresh 중 기존 cache key가 만료되어도 create 경로와 중복 계산하지 않도록 refresh도 createLock을 공유하게 변경했습니다 :)

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/test/java/com/example/solidconnection/cache/ThunderingHerdCachingAspectTest.java (1)

148-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

1. 실패 경로 검증용 카운터를 분리해두는 편이 좋겠습니다.

지금 callCount는 성공한 계산만 세어서 RuntimeException 시나리오가 실제로 refresh를 시도했는지 증명하지 못합니다. 메서드 진입 횟수를 따로 세면 fallback 테스트가 “시도 후 실패”를 더 정확히 잡아낼 수 있습니다.

예시 수정안
     static class TestCacheService {
 
+        private final AtomicInteger invocationCount = new AtomicInteger();
         private final AtomicInteger callCount = new AtomicInteger();
         private boolean failWithRuntimeException;
         private boolean failWithCustomException;
@@
         public String getValue(String key) {
+            invocationCount.incrementAndGet();
             if (failWithCustomException) {
                 throw new CustomException(ErrorCode.INVALID_INPUT);
             }
             if (failWithRuntimeException) {
                 throw new IllegalStateException("refresh failed");
@@
         int getCallCount() {
             return callCount.get();
         }
+
+        int getInvocationCount() {
+            return invocationCount.get();
+        }

이후 실패 케이스에서는 getInvocationCount()2인지 같이 확인하면 됩니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/example/solidconnection/cache/ThunderingHerdCachingAspectTest.java`
around lines 148 - 165, The test helper in ThunderingHerdCachingAspectTest
currently uses callCount only for successful returns, so the RuntimeException
fallback path is not proving that refresh was actually attempted. Add a separate
invocation counter in the test fixture’s getValue method to track every method
entry, keep callCount for successful value generation, and update the
failure-path assertions to verify the invocation count (for example via a
getInvocationCount() helper) so the “attempted then failed” behavior is
explicitly covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/example/solidconnection/util/RedisUtils.java`:
- Around line 61-64: The refresh-threshold check in RedisUtils should also
validate the percent range, not just nullability. Update the logic in the method
that compares leftTtl, defaultTtl, and percent so that invalid values like
percent <= 0 or percent > 100 are treated as false before computing the ratio.
Keep the existing guards around defaultTtl and leftTtl, and adjust the same
return path that currently uses percent to prevent misconfigured cache refresh
behavior.

---

Nitpick comments:
In
`@src/test/java/com/example/solidconnection/cache/ThunderingHerdCachingAspectTest.java`:
- Around line 148-165: The test helper in ThunderingHerdCachingAspectTest
currently uses callCount only for successful returns, so the RuntimeException
fallback path is not proving that refresh was actually attempted. Add a separate
invocation counter in the test fixture’s getValue method to track every method
entry, keep callCount for successful value generation, and update the
failure-path assertions to verify the invocation count (for example via a
getInvocationCount() helper) so the “attempted then failed” behavior is
explicitly covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: aa1d3118-dd8c-4712-928a-78f4ce272a04

📥 Commits

Reviewing files that changed from the base of the PR and between d3409d5 and 7aee8df.

📒 Files selected for processing (3)
  • src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java
  • src/main/java/com/example/solidconnection/util/RedisUtils.java
  • src/test/java/com/example/solidconnection/cache/ThunderingHerdCachingAspectTest.java

Comment thread src/main/java/com/example/solidconnection/util/RedisUtils.java Outdated
@Hexeong Hexeong self-assigned this Jun 27, 2026

@lsy1307 lsy1307 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

수고하셨습니다!

@Hexeong Hexeong merged commit 57a5b8a into develop Jun 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: ThunderingHerdCaching의 만료 임박 캐시 갱신 정책 수정

2 participants