문제 상황
이미지별 어노테이션 데이터를 조회하는 API를 만들었는데, 데이터 양이 커질수록 DB 쿼리 실행 시간이 늘어났다.
- 작은 데이터: 수십 ms
- 중간 데이터: 수백 ms
- 큰 데이터: 수백 개 쿼리 실행 → 응답 300ms 이상 튐
즉, 조회 성능과 안정성 부족이 문제였다.
해결 방안
1. Redis 캐시 도입
Spring Cache + Redis를 적용했다.
@Cacheable(value = "annotations:v1", key = "#imageId")
public AnnotationListResponse loadAnnotationsBy(Integer imageId) {
List<Annotation> annotations = getAnnotations(imageId);
return AnnotationListResponse.from(mapToResponses(annotations));
}
@Transactional
@CacheEvict(value = "annotations:v1", key = "#request.imageId")
public AnnotationResponse save(AnnotationCreateRequest request) {
Annotation annotation = saveAnnotation(...);
return AnnotationResponse.from(annotation);
}
- 조회: 캐시에 있으면 반환, 없으면 DB 조회 후 캐시에 저장
- 쓰기(저장/수정/삭제): DB 작업 후 해당 캐시 무효화
2. 성능 테스트 (JMeter)
| 케이스 | 평균 응답(ms) | p95 응답(ms) |
|---|---|---|
| 캐시 없음 | 97 | 378 |
| 캐시 있음 | 29 | 128 |
- 캐시 적용 시 평균 응답 70% 단축
- 대규모 데이터(hard 케이스)는 235ms → 62ms로 안정화
3. 캐시 전략 검증 (JMeter + Redis INFO)
-
Hot key 집중 (특정 imageId 반복)
- hit ratio 99.9% (처음만 DB, 이후 모두 캐시)
-
랜덤 분산 (1~1,000)
- hit ratio 약 90% (요청 1만 건 중 miss ≈ 1,000)
-
대규모 분산 (1~100,000)
- hit ratio 약 5% (거의 모든 요청이 miss)
관련 이론
Cache-aside 패턴
- 조회 시: 캐시 확인 → 없으면 DB 조회 후 캐시에 저장
- 쓰기 시: DB 변경 → 캐시 무효화(evict)
- 장점: 단순하고 관리 용이
- 단점: 첫 요청은 무조건 DB 조회 → cold start 문제 존재
캐시 Hit/Miss
- hit: 요청한 key가 캐시에 있어 빠르게 반환
- miss: 캐시에 없어 DB 조회 필요
-
공식:
hit ratio = hits / (hits + misses)
Hit율이 떨어지는 경우
- 랜덤/롱테일 패턴: key가 골고루 분산돼 대부분 한 번만 조회 → 캐시 효과 거의 없음
- TTL 짧음: 자주 만료돼 miss 증가
- 쓰기 잦음: evict 반복 → stale data 방지 때문에 hit율 ↓
- 메모리 부족: eviction 발생 → cold key가 날아가서 miss ↑
- 동시 만료(Cache Stampede): 인기 key TTL이 동시에 만료 → 대량 miss + DB 부하
Cache Stampede (캐시 스탬피드)
- 인기 key TTL 만료 순간 다수 요청 몰림 → 동시에 DB 접근
-
해결책:
- TTL에 random 지터 추가 (30분 ± 1~5분)
- 분산락(예: Redisson)으로 갱신 동시성 제어
- 미리 캐시 refresh (background refresh)
Eviction 정책
-
Redis
maxmemory-policy:allkeys-lru: 전체 key 중 가장 오래 안 쓴 것 삭제allkeys-lfu: 사용 빈도 낮은 것 삭제volatile-ttl: TTL 짧은 key 위주로 삭제
-
실무에서는 hot 데이터가 오래 남을 수 있도록 LFU/LRU 조합 사용
정리
이번 실험을 통해 단순히 캐시만 붙이는 게 아니라,
- 조회 성능 개선: 평균 응답시간 70% 단축, p95 지연시간 안정화
- 패턴별 효율 차이: hot key는 hit율 99%, 분산 요청은 hit율 5%
- 운영 전략 필요성: TTL, eviction, stampede 대응 없이는 실서비스 안정성 보장 어려움
→ “무엇을 캐시할지, 얼마나 유지할지” 전략 설계가 캐시 활용의 핵심이라는 걸 배웠다.