본문 바로가기
프로젝트 이슈

RunGuard 개발 - 캐시 무효화 검증

by BIGENGINEER 2026. 7. 10.

로컬 캐시 Caffeine으로 성능을 개선할 기반으로 마련했고, 이를 실제로 테스트 해보자

 

캐시 on/ off 성능 테스트 비교

 

1. MySQL에 테스트 데이터 넣기

mysql -u root -p runguard < k6/seed_weekly_report_perf.sql

 

2. 데이터 확인 

 

SELECT COUNT(*) AS users
FROM users
WHERE id BETWEEN 2 AND 102;

SELECT COUNT(*) AS workouts
FROM workouts
WHERE user_id BETWEEN 2 AND 102
  AND started_at BETWEEN '2026-07-04 00:00:00' AND '2026-07-10 23:59:59';

 

3. 캐시 OFF 서버 실행

.\gradlew.bat bootRun --args='--spring.profiles.active=cache-off'

 

4. 캐시 OFF k6 측정

k6 run --summary-export docs/performance/results/weekly-report-cache-off.json k6/runguard_weekly_report_k6.js

 

5. 서버 끄고 캐시 ON 서버 실행

.\gradlew.bat bootRun

 

6. 캐시 ON k6 측정

k6 run --summary-export docs/performance/results/weekly-report-cache-on.json k6/runguard_weekly_report_k6.js

 

구분                               요청 수           실패율               avg                              p95                             처리량

캐시 OFF 6,114 0.00% 96.18ms 179.21ms 86.96 req/s
캐시 ON 8,914 0.00% 2.48ms 4.35ms 126.99 req/s
  • 평균 응답 시간: 96.18ms -> 2.48ms, 약 97.4% 감소
  • p95 응답 시간: 179.21ms -> 4.35ms, 약 97.6% 감소 (95%가 이 시간 안에 처리됨)
  • 처리량: 86.96 req/s -> 126.99 req/s, 약 46.0% 증가 (서버가 1초 동안 처리한 요청 수)
  • 실패율: 둘 다 0%

 

캐시 on에서 응답 속도가 크게  빨라진 이유는?

캐시 off에서는 사용자 조회 -> 해당 기간 운동 데이터 조회 -> 운동 데이터 집계 ->주간 통계 계산 -> dto 변환 -> json 응답

그런데 데이터가 쌓일 수록 DB 쿼리와 집계 연산이 응답 시간 대부분을 차지함

 

반면에 캐시 on 상태에서는 최초 요청 이후에 계산 결과가 캐시에 저장됨

첫 번째 요청 → DB 조회 및 통계 계산 → 결과를 캐시에 저장 → 응답

두 번째 요청부터 → 캐시에서 결과 조회 → 바로 응답

 

 

 

< 캐시 히트 >

  • 평균 응답 시간: 96.18ms -> 2.48ms, 약 97.4% 감소 -> 이렇게 까지 빨라진 것을 보아 대부분의 요청이 캐시 히트였을듯
  • 캐시 히트율 = 캐시 히트 수 / 전체 캐시 조회 수 × 100

 


 

캐시 무효화 검증

- 캐시를 넣었는데 데이터가 오래가지 않도록 처리하는 것

- 즉, DB 변경됐을 때 기존의 값을 삭제하거나 최신 값으로 갱신하는 것

 

 

1. 주간 리포트 조회해서 현재 값 확인

2. 컨디션 이벤트 저장

3. 다시 주간 리포트 조회

4. conditionEventCount 또는 recentMemos가 바뀌는지 확인

 

 

PS C:\Users\orvos\RunGuard\RunGuard> Invoke-RestMethod "http://localhost:8080/api/reports/weekly?userId=2"


userId               : 2
startDate            : 2026-07-04
endDate              : 2026-07-10
workoutCount         : 7
totalDistanceM       : 42000
totalDurationSec     : 15120
averagePaceSec       : 360
anomalySegmentCount  : 0
conditionEventCount  : 2
mostFrequentBodyPart : RIGHT_CALF
recentMemos          : {weekly-report perf memo user 2 day 1, weekly-report perf memo user 2 day 4}



PS C:\Users\orvos\RunGuard\RunGuard> $body = @{
>>   segmentId = 20014
>>   eventType = "PAIN"
>>   bodyPart = "RIGHT_CALF"
>>   painLevel = 7
>>   fatigueLevel = 6
>>   environmentReason = "UPHILL"
>>   memo = "cache eviction verification"
>> } | ConvertTo-Json
PS C:\Users\orvos\RunGuard\RunGuard>
PS C:\Users\orvos\RunGuard\RunGuard> Invoke-RestMethod `
>>   -Uri "http://localhost:8080/api/workouts/2001/condition-events" `
>>   -Method Post `
>>   -ContentType "application/json" `
>>   -Body $body


id                : 1020050
workoutId         : 2001
segmentId         : 20014
eventType         : PAIN
bodyPart          : RIGHT_CALF
painLevel         : 7
fatigueLevel      : 6
environmentReason : UPHILL
memo              : cache eviction verification
createdAt         : 2026-07-10T11:43:22.7245286



PS C:\Users\orvos\RunGuard\RunGuard> Invoke-RestMethod "http://localhost:8080/api/reports/weekly?userId=2"


userId               : 2
startDate            : 2026-07-04
endDate              : 2026-07-10
workoutCount         : 7
totalDistanceM       : 42000
totalDurationSec     : 15120
averagePaceSec       : 360
anomalySegmentCount  : 0
conditionEventCount  : 2
mostFrequentBodyPart : RIGHT_CALF
recentMemos          : {weekly-report perf memo user 2 day 1, weekly-report perf memo user 2 day 4}

 

 

처음 무효화 검증했을 때 confitionEventCount의 변화가 없음 

 

 

<문제 원인 >

- ReportService의 캐시 키가  key = "'user:' + #userId" 이렇게 되어있었음

- 무효화는 user : 2 를 지우려고 하는데 런타임에서 userId 파라미터 이름을 못 잡을 수 있음

 

=> 그래서 위치 기반 파라미터로 바꿈  key = "'user:' + #p0"

 

 

 

중요

- 지금 이렇게 되면 모든 userId가 같은 캐시를 탔을 가능성이 있음 
그러면 지금 캐시 on에서 k6 성능 다시 테스트해보자

 

PS C:\Users\orvos\RunGuard\RunGuard> k6 run --summary-export docs/performance/results/weekly-report-cache-on-after-key-fix.json k6/runguard_weekly_report_k6.js

         /\      Grafana   /‾‾/
    /\  /  \     |\  __   /  /
   /  \/    \    | |/ /  /   ‾‾\
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/


     execution: local
        script: k6/runguard_weekly_report_k6.js
        output: -

     scenarios: (100.00%) 3 scenarios, 60 max VUs, 1m40s max duration (incl. graceful stop):
              * warmup: 1 looping VUs for 10s (gracefulStop: 30s)
              * baseline_10_vus: 10 looping VUs for 30s (startTime: 10s, gracefulStop: 30s)
              * load_50_vus: 50 looping VUs for 30s (startTime: 40s, gracefulStop: 30s)



  █ THRESHOLDS

    http_req_duration
    ✓ 'p(95)<500' p(95)=8.61ms

    http_req_failed
    ✓ 'rate<0.01' rate=0.00%


  █ TOTAL RESULTS

    checks_total.......: 17798   253.545549/s
    checks_succeeded...: 100.00% 17798 out of 17798
    checks_failed......: 0.00%   0 out of 17798

    ✓ weekly report status is 200
    ✓ weekly report has required fields

    CUSTOM
    weekly_report_duration.........: avg=2.891504 min=0        med=1.6143   max=91.3131  p(90)=4.1545   p(95)=8.6175
    weekly_report_failed...........: 0.00%  0 out of 8899

    HTTP
    http_req_duration..............: avg=2.89ms   min=0s       med=1.61ms   max=91.31ms  p(90)=4.15ms   p(95)=8.61ms
      { expected_response:true }...: avg=2.89ms   min=0s       med=1.61ms   max=91.31ms  p(90)=4.15ms   p(95)=8.61ms
    http_req_failed................: 0.00%  0 out of 8899
    http_reqs......................: 8899   126.772774/s

    EXECUTION
    iteration_duration.............: avg=204.09ms min=200.09ms med=202.44ms max=291.51ms p(90)=206.75ms p(95)=212.75ms
    iterations.....................: 8899   126.772774/s
    vus............................: 50     min=1         max=50
    vus_max........................: 60     min=60        max=60

    NETWORK
    data_received..................: 3.9 MB 55 kB/s
    data_sent......................: 872 kB 12 kB/s




running (1m10.2s), 00/60 VUs, 8899 complete and 0 interrupted iterations
warmup          ✓ [======================================] 1 VUs   10s
baseline_10_vus ✓ [======================================] 10 VUs  30s
load_50_vus     ✓ [======================================] 50 VUs  30s

 

 

구분                         요청 수         실패율                avg                    p95                      max                  처리량

캐시 OFF 6,114 0.00% 96.18ms 179.21ms 2.24s 86.96 req/s
캐시 ON 8,899 0.00% 2.89ms 8.61ms 91.31ms 126.77 req/s

 

개선 결과:

  • 평균 응답 시간: 96.18ms -> 2.89ms, 약 97.0% 감소
  • p95 응답 시간: 179.21ms -> 8.61ms, 약 95.2% 감소
  • 최대 응답 시간: 2.24s -> 91.31ms
  • 처리량: 86.96 req/s -> 126.77 req/s, 약 45.8% 증가
  • 실패율: 둘 다 0.00%