Skip to content

perf: 장소 상세를 캐시 위에 올린다 (외부가 멈춰도 502 대신 직전 값) - #256

Merged
sevineleven merged 7 commits into
devfrom
feat/poi-detail-cache
Aug 13, 2026
Merged

perf: 장소 상세를 캐시 위에 올린다 (외부가 멈춰도 502 대신 직전 값)#256
sevineleven merged 7 commits into
devfrom
feat/poi-detail-cache

Conversation

@sevineleven

@sevineleven sevineleven commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

어제 운영에서 이게 났다.

04:14:28 WARN TourApiClientImpl      TourAPI 공통상세 조회 실패
                                     cause=TimeoutException ... within 6000ms
04:14:28 WARN GlobalExceptionHandler 도메인 예외(5xx) code=TOUR-001 status=502

사용자가 코스에서 장소를 눌렀는데 상세 화면이 통째로 안 떴다.

detailCommon2 응답시간을 재봤다(2026-08-12, 표본 30건):

min p50 p90 p95 max
94ms 109ms 124ms 151ms 2,114ms

평시 110ms 짜리다. 6초를 넘겼다는 건 느려진 게 아니라 그 순간 응답이 안 온 것이다. 외부가 멈추는 것은 우리가 못 고친다. 하지만 그 노출을 줄이는 것은 우리 몫이다.

그리고 노출이 필요 이상으로 컸다. ExternalDataCache 를 날씨·대기질·버스·열차·지역콘텐츠가 다 쓰는데 장소 상세만 안 썼다. 같은 장소를 누를 때마다 외부를 두 번씩(공통상세 → 소개정보) 쳤고, 운영 로그에서 같은 contentId 가 40초 안에 세 번 조회되는 것을 봤다. 호출이 세 배면 멈춘 순간을 만날 확률도 세 배고, 일일 한도도 그만큼 탄다.

무엇을

같은 장소 재조회 매번 외부 2회 6시간 캐시
외부가 멈췄을 때 502 — 화면이 안 뜸 직전 값을 내림
실패한 뒤 매 요청이 각자 8초 대기 1분간 눌러두고 재시도

stale 을 허용한다. 상세는 주소·개요·운영시간이라 느리게 변하므로 6시간 전 값이 502 보다 낫다. 위 04:14 요청도 캐시가 있었다면 직전 값을 받았다.

TTL 은 값의 성격에서 도출한다

성공 6시간 느리게 변한다(주소·개요·운영시간)
조회 실패 1분 성공 TTL 로 누르면 그 장소가 6시간 죽는다. 아예 안 누르면 외부가 느린 동안 모든 요청이 각자 8초를 기다린다
없는 콘텐츠 10분 "없다" 는 안정된 답이지만, 우리가 코스에 실어 보낸 식별자가 404 면 그건 우리 쪽 버그 신호라 오래 굳히면 고친 뒤에도 404 가 나간다

키 공간 상한을 먼저 정했다

키는 TourAPI contentId 라 만 단위까지 갈 수 있다(89곳 실측 지역당 100건 안팎, 최대 평창군 446건). 실제로 눌리는 것은 코스에 실린 장소뿐이지만, TTL 은 엔트리를 지우지 않으므로(성능 규약) 상한을 2,000 으로 뒀다.

404 와 502 를 캐시 값이 구분한다

detail 만 담으면 "없는 콘텐츠(404)" 와 "조회 실패(502)" 가 둘 다 null 이 돼 섞인다. 클라이언트 계약이 갈리는 자리라 상태를 함께 담는다.

private record CachedDetail(PoiDetail detail, boolean lookupFailed) { ... }

인허가(LIC-)·국가유산(HER-) 식별자는 우리 DB 가 답하므로 캐시를 타지 않는다.

테스트

항목 결과
전체 1,206 passed (실패 0 · 건너뜀 21은 E2E 게이팅분)
신규 작성 PoiDetailIntegrationTest 4건
  • 같은 장소를 다시 조회하면 외부를 부르지 않는다 (호출 카운터로 단언)
  • 외부가 실패하고 직전 값도 없으면 502 다 — 조용히 빈 화면을 주지 않는다
  • 실패한 뒤 외부가 돌아오면 다시 받아온다 (실패를 성공으로 굳히지 않는다)
  • 국가유산 식별자는 외부를 아예 부르지 않는다

기존 13개 테스트 본문에 evictCache() 를 넣었다. 통합 테스트는 컨텍스트를 공유하므로 안 비우면 앞 테스트가 넣은 값으로 뒤 테스트가 통과한다 — 실제로 같은 contentId(126508·444)를 여러 테스트가 재사용하고 있었다. StubTourApiClient 에 상세 호출 카운터를 붙였다(기존 areaCalls 와 같은 방식).

작업을 마치기 전 자문 셋

① 운영에서 버티는가 — 테이블·인덱스·부팅 적재 변화 없음. 새로 드는 것은 힙뿐이고, 엔트리 2,000개 상한 × PoiDetail(개요 텍스트 포함) 이라 수 MB 규모다. 상한이 있어 트래픽에 비례해 자라지 않는다.

② 외부 API 한도를 갉아먹지 않나줄인다. 장소당 상세 호출이 6시간에 최대 2회(공통상세 + 소개정보)로 묶인다. 로그에서 본 "40초에 세 번" 이 한 번이 된다. 새로 소비하는 호출은 없다.

③ 코스의 완성도가 올라가는가 — 후보 수·커버리지는 그대로다. 바뀌는 것은 상세 화면이 외부 장애를 견디는 것이다. 지금은 외부가 멈춘 순간 502 라 카드가 통째로 비는데, 앞으로는 직전 값이 나간다.

범위 밖

  • timeout 6초는 그대로 둔다. p95 의 40배라 이미 관대하고, 줄이면 간헐 실패만 는다. 표본 30건이라 p99 를 신뢰할 수 없어(최대값 하나가 p99 가 되는 크기) 근거 없이 손대지 않는다.
  • 전역 컴포넌트 변경 없음GlobalExceptionHandler·SecurityConfig·공통 필터·ApiResponseBody 미변경.
  • 응답 계약 변경 없음PoiApi@ApiResponse 는 그대로다(200/404/502 그대로). 갱신 대상 아님.

머지 순서 메모

origin/dev 기준이고 PoiDetailService.java 만 건드린다. 현재 열린 스택(#249#250#251#252#253) 중 #253 같은 파일을 만진다 — new PoiDetail(...) 에 인자 한 줄을 더하는데, 이 PR 이 그 생성자 호출을 toPoiDetail() 로 옮기므로 한 덩어리 충돌이 난다. 스택은 어차피 앞이 머지될 때마다 base 가 바뀌므로, 이 PR 을 먼저 넣어도 추가 부담이 크지 않다.

Summary by CodeRabbit

  • 새 기능

    • 장소 상세 정보에 캐싱이 적용되어 동일한 정보를 반복 조회할 때 응답 속도가 향상됩니다.
    • 성공한 조회 결과는 일정 시간 보관되며, 외부 서비스 장애 시 기존 정보를 우선 제공합니다.
    • 조회 실패 및 정보 없음 결과를 구분해 각각 적절한 오류 응답을 제공합니다.
  • 버그 수정

    • 외부 상세 정보 조회 실패 후 서비스가 복구되면 자동으로 다시 조회합니다.
    • 국가유산 정보 조회 시 불필요한 외부 API 호출을 방지합니다.

같은 장소를 누를 때마다 외부를 두 번씩(공통상세 → 소개정보) 쳤다. 운영 로그에서
같은 contentId 가 40초 안에 세 번 조회되는 것을 봤다 — 호출이 세 배면 일일
한도도 세 배로 타고, 외부가 멈춘 순간을 만날 확률도 세 배다.

어제 그 순간이 실제로 왔다:

  04:14:28 WARN TourAPI 공통상세 조회 실패 cause=TimeoutException ... 6000ms
  04:14:28 WARN 도메인 예외(5xx) code=TOUR-001 status=502

detailCommon2 실측(표본 30건) p50 109ms · p95 151ms 라, 6초를 넘겼다는 것은
느려진 게 아니라 그 순간 응답이 안 온 것이다. 외부가 멈추는 것은 우리가 못
고치지만, 그 노출을 줄이는 것은 우리 몫이다.

stale 을 허용한다 — 상세는 주소·개요·운영시간이라 느리게 변하고, 6시간 전 값이
502 보다 낫다. 캐시가 있었다면 위 요청은 직전 값을 받았다.

TTL 은 값의 성격에서 도출한다. 성공 6시간 · 조회 실패 1분(재시도 유도) ·
없는 콘텐츠 10분. 실패를 성공 TTL 로 누르면 그 장소가 6시간 죽고, 아예 안
누르면 외부가 느린 동안 모든 요청이 각자 8초를 기다린다.

키 공간은 TourAPI contentId 라 만 단위까지 갈 수 있어 상한을 2,000 으로 둔다
— TTL 은 엔트리를 지우지 않는다(성능 규약).

"없는 콘텐츠(404)" 와 "조회 실패(502)" 를 캐시 값이 구분한다. detail 만 담으면
둘 다 null 이 돼 클라이언트 계약이 갈리는 자리에서 섞인다.

인허가·국가유산 식별자는 우리 DB 가 답하므로 캐시를 타지 않는다.

테스트는 공유 컨텍스트라 각 테스트가 본문에서 캐시를 비운다 — 안 비우면 앞
테스트가 넣은 값으로 뒤 테스트가 통과한다.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sevineleven, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 64 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c6d86bb8-b0f5-4fcd-b14d-cc2024d6d539

📥 Commits

Reviewing files that changed from the base of the PR and between 061ef48 and 3e8b526.

📒 Files selected for processing (5)
  • src/main/java/com/offway/core/common/logging/SensitiveParams.java
  • src/main/java/com/offway/core/trip/infrastructure/gallery/GalleryPhotoClientImpl.java
  • src/main/java/com/offway/core/trip/service/PoiDetailService.java
  • src/test/java/com/offway/core/common/logging/SensitiveParamsTest.java
  • src/test/java/com/offway/core/trip/controller/PoiDetailIntegrationTest.java
📝 Walkthrough

Walkthrough

PoiDetailService에 TourAPI 상세 조회 캐시가 추가되었습니다. 성공·실패·미존재 결과에 서로 다른 TTL을 적용합니다. 외부 조회 실패 시 stale 성공값을 반환하며, 통합 테스트가 캐시와 복구 동작을 검증합니다.

Changes

POI 상세 조회 캐시

Layer / File(s) Summary
캐시 상태와 TTL 정의
src/main/java/com/offway/core/trip/service/PoiDetailService.java
성공·실패·미존재 결과의 TTL과 캐시 설정을 추가했습니다. CachedDetail로 캐시 상태를 구분합니다.
캐시 기반 상세 조회 흐름
src/main/java/com/offway/core/trip/service/PoiDetailService.java
TourAPI 상세 조회를 캐시로 감쌉니다. 캐시된 실패와 미존재 결과를 각각 502와 404로 변환합니다. 외부 조회 실패 시 stale 성공값을 반환합니다. evictCache()로 전체 캐시를 비웁니다.
캐시 동작 통합 검증
src/test/java/com/offway/core/trip/controller/PoiDetailIntegrationTest.java, src/test/java/com/offway/core/trip/infrastructure/tour/StubTourApiClient.java
중복 조회 방지, 실패 응답, 외부 복구 후 재조회, 국가유산 조회의 외부 API 미호출을 검증합니다. 테스트 클라이언트가 상세 조회 횟수를 추적합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PoiDetailService
  participant ExternalDataCache
  participant TourApiClient
  Client->>PoiDetailService: POI 상세 조회 요청
  PoiDetailService->>ExternalDataCache: 캐시 조회
  ExternalDataCache->>TourApiClient: 캐시 누락 시 findDetail 호출
  TourApiClient-->>ExternalDataCache: 상세 결과 또는 실패
  ExternalDataCache-->>PoiDetailService: 캐시 상태 반환
  PoiDetailService-->>Client: 200, 404 또는 502 응답
Loading

Possibly related PRs

  • team-offway/core#71: 동일한 PoiDetailService와 통합 테스트 흐름을 확장합니다.
  • team-offway/core#234: PoiDetailService의 국가유산 식별자 기반 조회와 직접 연결됩니다.
  • team-offway/core#235: PoiDetailService의 TourAPI 상세 조회와 PoiDetail 변환을 함께 수정합니다.

Suggested labels: feat

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 장소 상세 캐시 적용과 외부 장애 시 직전 값 반환이라는 주요 변경 사항을 명확하게 설명합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 feat/poi-detail-cache

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.

@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: 2

🧹 Nitpick comments (1)
src/test/java/com/offway/core/trip/controller/PoiDetailIntegrationTest.java (1)

316-333: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

TTL과 stale 정책을 직접 검증하는 테스트를 추가하면 좋겠습니다.

첫 테스트는 성공 응답 뒤에 evictCache()를 호출합니다. 따라서 실패 loader에 stale 성공값이 전달되지 않고, 실제 검증 결과도 502입니다. 두 번째 테스트도 실패 캐시를 비운 뒤 복구를 확인합니다. 따라서 1분 실패 TTL이 지난 뒤 자동 재시도하는 동작을 검증하지 못합니다.

  • src/test/java/com/offway/core/trip/controller/PoiDetailIntegrationTest.java#L316-L333: 성공 캐시를 만료시킨 뒤 외부 실패를 만들고, stale 성공값으로 200을 반환하는지 검증하면 좋겠습니다.
  • src/test/java/com/offway/core/trip/controller/PoiDetailIntegrationTest.java#L337-L352: 실패 캐시 TTL이 지난 뒤 evictCache() 없이 외부를 다시 호출하고 200으로 복구하는지 검증하면 좋겠습니다.
🤖 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/offway/core/trip/controller/PoiDetailIntegrationTest.java`
around lines 316 - 333, Update
src/test/java/com/offway/core/trip/controller/PoiDetailIntegrationTest.java
lines 316-333 so the test expires the successful cache entry, triggers an
external failure without clearing stale data, and asserts the stale value is
returned with HTTP 200. Update lines 337-352 so the test waits beyond the
one-minute failure TTL, retries without calling evictCache(), and verifies the
external request succeeds with HTTP 200, covering automatic recovery.
🤖 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/offway/core/trip/service/PoiDetailService.java`:
- Around line 167-173: Update the failure logs in PoiDetailService’s stale-value
and no-stale-value branches to stop logging the raw contentId user input. Remove
the contentId field unless identification is required; if retained, mask it
through the shared SensitiveParams utility.
- Around line 103-115: CachedDetail의 성공·미존재·조회 실패 상태를 detail null 여부와
lookupFailed boolean 조합 대신 DetailStatus enum으로 표현하도록 변경하세요. CachedDetail에 상태 필드를
추가하고 found, notFound, failed 팩토리 메서드가 각각 FOUND, NOT_FOUND, LOOKUP_FAILED를 설정하게
하며, 기존 상태 판별 로직도 해당 enum을 사용하도록 갱신하세요.

---

Nitpick comments:
In `@src/test/java/com/offway/core/trip/controller/PoiDetailIntegrationTest.java`:
- Around line 316-333: Update
src/test/java/com/offway/core/trip/controller/PoiDetailIntegrationTest.java
lines 316-333 so the test expires the successful cache entry, triggers an
external failure without clearing stale data, and asserts the stale value is
returned with HTTP 200. Update lines 337-352 so the test waits beyond the
one-minute failure TTL, retries without calling evictCache(), and verifies the
external request succeeds with HTTP 200, covering automatic recovery.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a97e143c-aece-4301-8fa5-bda095d773cd

📥 Commits

Reviewing files that changed from the base of the PR and between a9d7d0a and 061ef48.

📒 Files selected for processing (3)
  • src/main/java/com/offway/core/trip/service/PoiDetailService.java
  • src/test/java/com/offway/core/trip/controller/PoiDetailIntegrationTest.java
  • src/test/java/com/offway/core/trip/infrastructure/tour/StubTourApiClient.java

Comment thread src/main/java/com/offway/core/trip/service/PoiDetailService.java Outdated
Comment thread src/main/java/com/offway/core/trip/service/PoiDetailService.java Outdated
`detail == null` 과 `lookupFailed` 를 함께 읽어야 상태를 알 수 있었다. 셋의 클라이언트
계약이 각각 다른데(200·404·502) 판별이 두 필드의 조합에 흩어져 있어, 새 상태를 더하면
조합이 늘고 잘못된 조합(`detail != null` 인데 실패)도 표현 가능했다.

`DetailStatus`(FOUND·NOT_FOUND·LOOKUP_FAILED) 로 상태를 이름으로 들고, 무엇을 내릴지는
`CachedDetail.orThrow()` 가 switch 로 소유한다. 서비스에 남아 있던 상태 해석 분기 두 개가
사라졌다. `found()` 는 detail 을 requireNonNull 로 받아 "FOUND 인데 값이 없는" 조합을 막는다.
경로 변수 `contentId` 는 서블릿이 퍼센트 디코딩을 마친 값이라 `%0A` 가 **실제 개행**으로
온다. 그대로 log.warn 에 실으면 값 하나가 로그를 여러 줄로 쪼개, 있지도 않은 로그 줄을
지어낼 수 있다(log forging). 길이 상한도 없어 값 하나가 줄 전체를 밀어낸다.

쿼리스트링(`readableParams`)과 예외 메시지(`RootCause`)는 이미 제어문자 제거·길이 제한을
거치는데, **값 하나**를 찍는 경로에만 그 기준이 없었다. `SensitiveParams.forLog(String)` 로
같은 기준을 노출하고(디코딩은 하지 않는다 — 이미 디코딩된 값을 또 풀면 `%41` 이 `A` 가 돼
로그가 실제 요청과 다른 값을 가리킨다), 같은 모양의 자리를 함께 고쳤다.

- `PoiDetailService` 상세 조회 실패 로그 2곳 (경로 변수)
- `GalleryPhotoClientImpl` 필수 값 누락 로그 (외부 응답 문자열)

값을 **가리지는 않는다.** contentId 는 공개 콘텐츠 식별자이고 우리가 코스 응답에 실어
보내는 값이라, 마스킹하면 "어느 장소가 degrade 했나" 에 답하지 못해 로그의 존재 이유가
사라진다. 레포도 regionId·contentTypeId 를 같은 기준으로 남기고 있다.
`외부가_실패해도_직전_값을_내린다` 는 이름과 달리 본문이 캐시를 비운 뒤 502 를 단언한다.
이름만 읽으면 stale 경로가 덮인 줄 알아, 정작 그 경로가 깨져도 아무도 모른다.

단언하는 것(직전 값이 없으면 502)으로 이름을 바꾸고, 반대편(직전 값이 있으면 내린다)을
여기서 재현하지 않는 이유를 문서에 적었다 — TTL 이 6시간이라 만료를 기다릴 수 없고 시계
seam 도 없다. stale-while-error 자체는 `ExternalDataCacheTest` 가 짧은 TTL 로 덮는다.
@sevineleven

Copy link
Copy Markdown
Contributor Author

@coderabbitai nitpick(PoiDetailIntegrationTest#L316-L333, #L337-L352) 처리 결과입니다.

관찰은 맞고, 제안한 테스트는 넣지 않았습니다.

지적대로 첫 테스트는 이름이 외부가_실패해도_직전_값을_내린다 인데 본문은 캐시를 비운 뒤 502 를 단언 합니다. 이름과 단언이 어긋나 있었습니다.

다만 제안한 두 테스트(성공 캐시 만료 후 stale 200, 실패 TTL 경과 후 자동 복구)는 지금 구조에서 넣을 수 없습니다.

  • 성공 TTL 6시간·실패 TTL 1분이고, ExternalDataCache 에 시계를 주입하는 seam 이 없습니다. 실시간으로 만료를 기다리면 통합 테스트 한 건이 최소 1분을 태웁니다.
  • TTL 을 테스트용으로 낮추는 설정 훅을 새로 뚫는 것은 테스트를 위해 운영 코드에 seam 을 파는 것 이라 값에 비해 비쌉니다.
  • 무엇보다 그 동작은 이미 덮여 있습니다. stale-while-error 와 만료 후 재조회는 캐시 프리미티브의 단위 테스트가 40ms TTL 로 검증합니다 — ExternalDataCacheTest#조회_실패시_마지막_성공값을_돌려준다_stale_while_error, #loader가_예외를_던져도_요청_경로로_올리지_않고_stale로_degrade한다, #만료되면_다시_조회한다. 통합 테스트에서 확인할 것은 "이 서비스가 그 정책(ALLOW_STALE)을 골라 쓰는가" 이고, 정책이 갈리는 지점인 직전 값 없음 → 502 가 그 테스트입니다.

대신 이름·문서를 단언하는 것과 맞췄습니다 (30084f4): 외부가_실패했는데_직전_값도_없으면_502로_알린다 로 바꾸고, 반대편을 여기서 재현하지 않는 이유와 어디서 덮이는지를 javadoc 에 적었습니다. 이름만 읽고 stale 경로가 덮인 줄 아는 상태가 더 위험하다고 봤습니다.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== commit =='
git show --stat --oneline 30084f4

echo
echo '== changed integration test / Javadoc =='
git show --format=fuller --find-renames -- \
  src/test/java/com/offway/core/trip/controller/PoiDetailIntegrationTest.java \
  src/main/java/com/offway/core/trip/service/PoiDetailService.java \
  30084f4

echo
echo '== relevant symbol locations =='
rg -n -C 5 \
  '외부가_실패했는데_직전_값도_없으면_502로_알린다|ALLOW_STALE|stale-while|만료.*다시.*조회|마지막_성공값|loader가_예외' \
  src/main/java src/test/java

echo
echo '== focused outlines =='
ast-grep outline src/main/java/com/offway/core/trip/service/PoiDetailService.java --items all --type method
fd -a 'ExternalDataCacheTest.java' src/test/java -x ast-grep outline {} --items all --type method

Length of output: 34843


@sevineleven 확인했습니다. 30084f4에서 테스트 이름과 502 단언이 일치합니다.

PoiDetailService#tourDetailStalePolicy.ALLOW_STALE를 명시적으로 사용합니다. ExternalDataCacheTest는 만료 후 재조회, stale-while-error, loader 예외 시 stale 반환을 각각 검증합니다. 따라서 6시간 TTL을 기다리는 통합 테스트를 추가하지 않아도 현재 테스트 계층의 책임 분리가 명확합니다.

Javadoc도 이 통합 테스트가 검증하는 범위와 제외한 범위를 정확히 설명합니다. 이 nitpick은 해결되었습니다.

🐇 ✅


🧠 Learnings used
Learnt from: CR
Repo: team-offway/core PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-08-06T00:40:32.474Z
Learning: Applies to src/main/java/**/*.java : 외부 호출은 `ExternalDataCache` 위에 구성하고, 데이터 특성에 근거한 TTL·stale 정책·폴백을 설계한다.

Learnt from: sevineleven
Repo: team-offway/core PR: 99
File: CLAUDE.md:0-0
Timestamp: 2026-07-30T02:47:34.368Z
Learning: OffWay core의 `ExternalDataCache.StalePolicy`는 캐시 만료 공백 처리 정책을 소유한다. 느리게 변하는 데이터는 `ALLOW_STALE` 기반 stale-while-revalidate를 사용하고, 실시간 데이터는 `DISALLOW_STALE`로 stale 데이터를 제공하지 않고 조회 불가로 처리한다.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@sevineleven
sevineleven merged commit 640fa37 into dev Aug 13, 2026
4 checks passed
@sevineleven
sevineleven deleted the feat/poi-detail-cache branch August 13, 2026 16:06
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