이벤트 상세 조회 API에 @Cacheable
을 적용하여 조회 시마다 DB 접근을 줄이고 응답 속도를 높이려 했습니다. 그러나 실제 서비스에서 적용 후, 조회수 랭킹이 갱신되지 않는 문제가 발생했습니다.
캐시된 응답이 반환되면서, 실제 조회수 증가 처리를 담당하는 로직이 실행되지 않았습니다.
특히 조회수 랭킹은 Redis의 ZSet을 기반으로 집계되고 있었는데, @Cacheable
로 인해 API 내부의 ZINCRBY
호출이 누락되었습니다.
이로 인해, 실제로 해당 이벤트를 조회했더라도 조회수가 증가하지 않아 랭킹에는 반영되지 않는 문제가 지속적으로 발생했습니다.
@Cacheable
애너테이션을 제거하고, 캐시 로직을 수동으로 제어하는 방식으로 전환했습니다.@Cacheable(value = "eventDetail", key = "#eventId")
public EventDetailRes getEventDetail(Long eventId) {
// 1. 쿠키를 통해 중복 조회 확인
if (!isDuplicateView(request, response, eventId)) {
incrementViewCount(eventId); // 2. 조회수 증가
}
// 3. 이벤트 조회
Event event = eventRepository.findById(eventId)
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_NOT_FOUND));
// 4. 응답 객체 변환
return EventDetailRes.toEventDetailRes(event);
}
/**
* 이벤트 상세 조회 및 조회수 증가
*/
public EventDetailRes getEventDetail(HttpServletRequest request, HttpServletResponse response, Long eventId) {
// 1. 캐시 키 생성
String cacheKey = RedisKeyHelper.getEventDetailCacheKey(eventId);
// 2. 캐시에서 데이터 확인
EventDetailRes cachedEvent = (EventDetailRes) redisTemplate.opsForValue().get(cacheKey);
// 3. 쿠키를 통해 중복 조회 확인
if (!isDuplicateView(request, response, eventId)) {
incrementViewCount(eventId); // 조회수 증가
}
// 4. 캐시가 존재하면 반환
if (cachedEvent != null) {
return cachedEvent;
}
// 5. 캐시가 없으면 DB 조회
Event event = eventRepository.findById(eventId)
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_NOT_FOUND));
// 6. 변환 및 캐시에 저장
EventDetailRes eventDetailRes = EventDetailRes.toEventDetailRes(event);
redisTemplate.opsForValue().set(cacheKey, eventDetailRes, Duration.ofHours(24)); // 24시간 TTL
return eventDetailRes;
}
이번 이슈를 통해 캐시 적용 시 정합성과 성능의 균형을 어떻게 맞출 것인지에 대한 중요성을 다시금 실감했습니다.
특히 조회수처럼 변동이 잦은 데이터는 캐시 전략을 신중하게 설계해야 하며, 상황에 따라 수동 캐시 제어 방식이 더 유리할 수 있음을 확인할 수 있었습니다.