핵심 발견: 캐시를 켰는데 오히려 더 느려지는 구간이 있었음. 원인은 DB나 Redis가 아니라 캐시 직렬화 방식이었음 — 고치니 10~30배 개선.
1단계 — 캐시 없이 측정
잔디(월별/연간/수강량) 조회는 마이페이지 진입 시 거의 항상 호출되는 엔드포인트. 캐시를 붙이기 전, DB에서 직접 학습 기록을 집계하는 상태로 먼저 측정. 중간 강도 부하(10,000명, 30초 분산 도착)에서 yearly 280ms, lessons 766ms — 느림. "캐시를 붙이면 당연히 빨라질 것"이라는 가정으로 다음 단계 진행.
2단계 — 캐시 적용 후 측정 → 예상과 반대로 더 느려짐
@Cacheable로 Redis 캐시(TTL 10분)를 붙이고 동일 조건 재측정. 그런데 캐시를 켰는데 오히려 더 느려짐: yearly 280ms → 1,809ms, lessons 766ms → 1,681ms. "캐시가 손해"라는, 직관에 반하는 결과가 나옴. monthly는 27ms로 거의 그대로라 캐시 자체가 고장난 건 아님 — 특정 엔드포인트만 영향을 받는다는 단서.
3단계 — 원인 파악
영향받은 두 엔드포인트(yearly/lessons)의 공통점은 365일치를 통째로 캐싱하는, 페이로드가 큰(21KB+) 캐시라는 것. RedisConfig를 보니 모든 캐시에 GenericJackson2JsonRedisSerializer + ObjectMapper.DefaultTyping.EVERYTHING이 전역 적용 중 — 캐시에 들어가는 모든 객체·필드에 타입 메타데이터(@class)가 박혀서, 큰 페이로드일수록 역직렬화 비용이 비선형적으로 커짐. "캐시가 느려진 게 아니라, 직렬화가 느린 것"이라는 가설을 세움.
4단계 — 직렬화를 가볍게 바꾸고 재측정 → 가설 확인
잔디 캐시만 타입을 고정한 가벼운 직렬화(Jackson2JsonRedisSerializer<구체타입>)로 교체해서 동일 조건 재측정. yearly 1,809ms → 26ms(10.8배), lessons 1,681ms → 22ms(34.8배)로 정상화. 가설이 맞았음을 확인 — 캐시는 처음부터 정상 동작 중이었고, 문제는 "캐시값을 다시 객체로 만드는 비용"이었음.
| 항목 | 값 |
|---|---|
| 더미데이터 | 회원 10,000명, daily_study_stats 665만 행 (1인당 최대 730일) |
| 부하 패턴 | 10,000명이 각자 1번씩 조회, 분산 도착 (k6 ramping-arrival-rate) |
| 캐시 | Redis (Docker), TTL 10분 |
| 측정 방식 | 코드 분기(캐시 없음 / 캐시+무거운 직렬화 / 캐시+가벼운 직렬화)로 비교, k6 터미널 + Grafana 실시간 대시보드 + Datadog APM Trace로 캡처 |
| 엔드포인트 | 캐시 없음 | 캐시 + 기존(무거운) 직렬화 | 캐시 + 가벼운 직렬화 |
|---|---|---|---|
/api/grass/yearly |
AVG 696.2ms / P95 1.4s | AVG 388.0ms / P95 994.7ms (트렌드 최대 2s) | AVG 12.5ms / P95 24.2ms |
/api/grass/lessons |
AVG 27.4ms / P95 69.3ms | AVG 386.2ms / P95 942.2ms ⚠️ | AVG 19.7ms / P95 41.6ms |
/api/grass/monthly |
AVG 10.2ms / P95 14.3ms | AVG 13.6ms / P95 21.5ms | AVG 12.6ms / P95 16.6ms |
⚠️ = 캐시를 켰는데 캐시 없을 때보다 더 느려진 구간 (lessons는 캐시+무거운 직렬화가 캐시 없음보다 13배 이상 느림)
왜 yearly/lessons만 영향이 컸나: monthly는 30일치라 페이로드가 작아서 타입 메타데이터 무게가 잘 안 느껴짐. yearly/lessons는 365일치를 통째로 캐싱해서 페이로드가 크고, 그 안의 모든 필드에 타입 정보가 중첩되면서 역직렬화 비용이 비선형적으로 커짐.