해당 문서는 메인 화면의 섭취 기록 캘린더 API 응답 속도를 최적화하기 위한 성능 개선 문서입니다. 주요 목표는 아래와 같습니다.
- 반복적인 애플리케이션 계산 비용 제거
- 캐시 구조 최적화로 캐시 무효화 최소화, hit rate 향상
- 부하 테스트(k6)를 근거로 캐시 update 이후 응답 시간 수치 개선 (
p95기준 30% 단축)
- N+1 현상을 fetch join 으로 한 번에 가져오도록 변경
- 쿼리 자체에는 큰 성능 병목이 없고, DB 인덱스 설계도 적절
- 특정 월의 모든 일일 섭취량을 가져오는 캘린더 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, 위 방식은 회원이 많아졌을 때 불필요한 레코드가 쌓여서 용량 관리에 단점이 있었음 → 애플리케이션 단의 계산으로 유지
- Spring Cache 기반 단순 캐시
@Cachable어노테이션을 통한 단순String적재 - 섭취 기록 1건 추가 시 캘린더 전체 캐시
@CacheEvict - 즉, 사용자가 한 끼만 기록해도 한 달치 계산해놓은 기존 캐시 삭제 +
TreeMap재계산 반복 - 사용자가 하루 3~4회 섭취 기록 write 시 캐시 효율 나쁨 → 오히려 지속적인 계산 비용만 소모한다.
애플리케이션 로직 측면에서, 월 30일 정도의 TreeMap 계산 자체는 효율이 나쁘지 않다.
병목 원인은 write 할 때마다 캐시가 무효화되어 매번 Cache-miss & 재계산이 발생한다는 점이다.
→ DB 저장 구조를 변경하거나 극한의 계산 로직을 최적화하는 것보다, 캐시 구조를 근본적으로 개선하고 hit-rate를 높이는 것이 투자 비용 대비 효과가 크다.
→ 캐시 구조를 세분화하여 write가 발생해도 전체 캐시가 무효화되지 않도록 설계한다. 핵심은 캐시 무효화 범위를 ‘월 전체’에서 ‘해당 날짜’로 축소하는 것이다.
- 기존: Key 단위 → calendar:{userId}:{yearMonth}
- 변경: Hash 구조로 개별 날짜 필드 관리, 갱신 (과거 기록은 그대로 유지)
Key : user:{userId}:intake:{yyyy-MM}
Type : Redis Hash
Field : yyyy-MM-dd
Value : JSON(IntakeSummaryResponse)
- 현재 전략: TTL 고정 (예: 30일)
- 변경 전략: 조회 시 TTL 갱신으로 자주 보는 최신 데이터는 TTL 유지, 과거 데이터는 삭제
| 정책 | 설명 |
|---|---|
volatile-lru |
TTL 있는 키 중 LRU 제거 |
volatile-ttl |
TTL 가장 짧은 키 제거 |
allkeys-lru |
전체 키 중 LRU 제거 |
noeviction |
기본값, 메모리 초과 시 에러 |
volatile-lfu |
사용 횟수 기반 제거 |
운영 환경의 메모리 상황에 맞춰 정책을 선택할 수 있다. 이전의 TTL 전략과 맞춰서 volatile-ttl 옵션을 사용했다.
- 기존 구조는 계산 로직은 적절한 편이나, 캐시 전략이 비효율적이었다.
- 캐시 단위를 해당 월-날짜 별로 세분화하고 TTL 전략을 도입하여 아래와 같은 결과를 얻었다.
- 고비용 로직의 계산 빈도 감소
- 메인 화면 캘린더 API 응답 속도 개선
- 시스템 부하 감소, hit-rate 상승