Skip to content

Latest commit

 

History

History
101 lines (71 loc) · 4.69 KB

File metadata and controls

101 lines (71 loc) · 4.69 KB

캐시 전략 최적화로 메인 조회 API 병목 제거

개요

해당 문서는 메인 화면의 섭취 기록 캘린더 API 응답 속도를 최적화하기 위한 성능 개선 문서입니다. 주요 목표는 아래와 같습니다.

  • 반복적인 애플리케이션 계산 비용 제거
  • 캐시 구조 최적화로 캐시 무효화 최소화, hit rate 향상
  • 부하 테스트(k6)를 근거로 캐시 update 이후 응답 시간 수치 개선 (p95 기준 30% 단축)

1. 문제 인식

최적화 접근

Database 계층

  • N+1 현상을 fetch join 으로 한 번에 가져오도록 변경
  • 쿼리 자체에는 큰 성능 병목이 없고, DB 인덱스 설계도 적절

Application 로직

  • 특정 월의 모든 일일 섭취량을 가져오는 캘린더 API 계산 비용이 높음
  • 사용자의 상태 이력, 활동량, 섭취 로그 등 다양한 테이블의 데이터를 조합해야 함
  • 캐시 미스 시 매번 TreeMap 재생성 + 해당 월의 모든 날짜에 대한 섭취량 계산이 반복됨

기존 로직 시퀀스는 아래와 같다.

[1] 월 전체 날짜 생성
[2] 가입일 ~ 해당월 말일까지 MemberStatus 모두 조회 → TreeMap 생성
[3] 활동량(ActivityLevel) 전체 조회 → Map<Long, ActivityLevel>
[4] 해당 월 섭취 로그 조회 → Map<LocalDate, IntakeLog>
[5] 날짜 루프 돌며 IntakeSummary 생성
    [A] 섭취 기록 존재: withIntakeLog
    [B] 기록 없음: 상태 이력 + 활동량으로 goalKcal 계산
[6] List<IntakeSummaryResponse> 응답
  • 한 달마다 해당 로직이 반복되는 것을 볼 수 있다.
  • 회원의 활동량(ActivityLevel)과 상태기록(MemberStatus)이 수시로 변경될 수 있다.
    • 해당 변경은 goalKcal 계산에 큰 영향을 주는 변수이기 때문에 로직 변경이 불가능함
    • TreeMap에서 floorEntry를 돌며 변경 지점을 찾는 대신, DB에 모든 날짜별 정보를 저장하여 계산을 단순화 하는 방법도 존재
    • But, 위 방식은 회원이 많아졌을 때 불필요한 레코드가 쌓여서 용량 관리에 단점이 있었음 → 애플리케이션 단의 계산으로 유지

Cache 계층 (기존 캐시 전략의 문제점)

  • Spring Cache 기반 단순 캐시 @Cachable 어노테이션을 통한 단순 String 적재
  • 섭취 기록 1건 추가 시 캘린더 전체 캐시 @CacheEvict
  • 즉, 사용자가 한 끼만 기록해도 한 달치 계산해놓은 기존 캐시 삭제 + TreeMap 재계산 반복
  • 사용자가 하루 3~4회 섭취 기록 write 시 캐시 효율 나쁨 → 오히려 지속적인 계산 비용만 소모한다.

2. 개선 전략

애플리케이션 로직 측면에서, 월 30일 정도의 TreeMap 계산 자체는 효율이 나쁘지 않다. 병목 원인은 write 할 때마다 캐시가 무효화되어 매번 Cache-miss & 재계산이 발생한다는 점이다.

→ DB 저장 구조를 변경하거나 극한의 계산 로직을 최적화하는 것보다, 캐시 구조를 근본적으로 개선하고 hit-rate를 높이는 것이 투자 비용 대비 효과가 크다.

→ 캐시 구조를 세분화하여 write가 발생해도 전체 캐시가 무효화되지 않도록 설계한다. 핵심은 캐시 무효화 범위를 ‘월 전체’에서 ‘해당 날짜’로 축소하는 것이다.

2-1. 캐시 구조 변경 (HSET 도입)

  • 기존: Key 단위 → calendar:{userId}:{yearMonth}
  • 변경: Hash 구조로 개별 날짜 필드 관리, 갱신 (과거 기록은 그대로 유지)
Key   : user:{userId}:intake:{yyyy-MM}
Type  : Redis Hash
Field : yyyy-MM-dd
Value : JSON(IntakeSummaryResponse)

2-2. TTL 전략

  • 현재 전략: TTL 고정 (예: 30일)
  • 변경 전략: 조회 시 TTL 갱신으로 자주 보는 최신 데이터는 TTL 유지, 과거 데이터는 삭제

2-3. Redis 메모리 관리 정책 설정

정책 설명
volatile-lru TTL 있는 키 중 LRU 제거
volatile-ttl TTL 가장 짧은 키 제거
allkeys-lru 전체 키 중 LRU 제거
noeviction 기본값, 메모리 초과 시 에러
volatile-lfu 사용 횟수 기반 제거

운영 환경의 메모리 상황에 맞춰 정책을 선택할 수 있다. 이전의 TTL 전략과 맞춰서 volatile-ttl 옵션을 사용했다.


결론

  • 기존 구조는 계산 로직은 적절한 편이나, 캐시 전략이 비효율적이었다.
  • 캐시 단위를 해당 월-날짜 별로 세분화하고 TTL 전략을 도입하여 아래와 같은 결과를 얻었다.
    • 고비용 로직의 계산 빈도 감소
    • 메인 화면 캘린더 API 응답 속도 개선
    • 시스템 부하 감소, hit-rate 상승