Skip to content
/ FoodOn Public

AI 음식 이미지 판별을 통한 개인 식단 관리 서비스

Notifications You must be signed in to change notification settings

Pearl-K/FoodOn

Repository files navigation

캐시 전략 최적화로 메인 조회 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 상승

About

AI 음식 이미지 판별을 통한 개인 식단 관리 서비스

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors