잔디(Grass) Redis 캐싱 — Before/After 검증

핵심 발견: 캐시를 켰는데 오히려 더 느려지는 구간이 있었음. 원인은 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로 캡처

메인 결과 — 엔드포인트별 3종 비교 (Grafana 실시간 캡처)

엔드포인트 캐시 없음 캐시 + 기존(무거운) 직렬화 캐시 + 가벼운 직렬화
/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일치를 통째로 캐싱해서 페이로드가 크고, 그 안의 모든 필드에 타입 정보가 중첩되면서 역직렬화 비용이 비선형적으로 커짐.