GPT 중복 호출 방지
발생 상황
장소 추천 API(GET /api/place/recommend)는 사용자의 사주 기반 일일 에너지를 분석해 GPT로 장소 키워드를 생성합니다.
구현 후 API 호출 패턴을 검토하다가 문제를 발견했습니다. 메인 화면에서 장소 추천 카드를 렌더링할 때마다 이 API가 호출되는데, 같은 날 같은 사용자의 사주 에너지는 항상 동일하므로 GPT 응답도 동일합니다. 그런데 매 요청마다 GPT API를 새로 호출하고 있었습니다.
GPT API 호출에는 두 가지 비용이 따릅니다.
- 응답 시간: 평균 2~5초의 지연이 발생해 화면 렌더링이 느려집니다.
- API 비용: 요청마다 토큰 비용이 발생하며, 사용자가 많아질수록 선형으로 증가합니다.
해결 방법
@Cacheable을 사용해 GPT 응답을 인메모리 캐시에 저장합니다. 같은 날 같은 사용자의 요청은 캐시에서 즉시 반환하고 GPT 호출을 건너뜁니다.
캐시 키 설계
캐시 키 = userId + ':' + 오늘 날짜 (yyyy-MM-dd)
예시: 42:2025-06-16
날짜를 키에 포함시켜 자정이 지나면 키가 자동으로 달라집니다. 별도로 캐시를 비우거나 무효화하는 스케줄러 없이 매일 새 GPT 응답이 자연스럽게 생성됩니다.
Caffeine 캐시 설정
| 설정 | 값 | 이유 |
|---|---|---|
| TTL | 24시간 | 날짜 키 설계로 실질적 갱신이 이뤄지지만, 혹시 모를 메모리 누적 방지 |
| 최대 항목 수 | 10,000개 | 사용자 수 증가 시 OOM 방지, 초과 시 LRU 자동 퇴출 |
요청 처리 흐름
요청 수신 (userId=42, date=2025-06-16)
│
캐시 조회 (키: 42:2025-06-16)
├─ HIT → 캐시 결과 즉시 반환 (GPT 호출 없음, ~수ms)
└─ MISS → 일일 에너지 DB 조회
↓
GPT API 호출 (~2~5초)
↓
결과를 캐시에 저장
↓
클라이언트에 반환
이 방식을 선택한 이유
Redis vs Caffeine
Redis는 분산 캐시라 서버가 여러 대로 확장되어도 캐시를 공유할 수 있습니다. 하지만 현재는 단일 EC2 서버로 운영하는 MVP 단계입니다. Redis를 도입하려면 ElastiCache 설정, 직렬화, 네트워크 비용이 추가됩니다. Caffeine은 JVM 내부에서 동작하므로 네트워크 왕복이 없고 설정이 단순합니다.
추후 서버가 2대 이상으로 확장되면 인스턴스마다 캐시가 분리되어 GPT 중복 호출이 다시 발생할 수 있습니다. 이 경우 @Cacheable 인터페이스는 그대로 유지하면서 CacheManager 구현체만 Caffeine에서 Redis로 교체하면 됩니다. 비즈니스 로직 코드를 수정하지 않고 전환할 수 있도록 설계했습니다.
캐시 키에 날짜를 포함한 이유
자정에 캐시를 비우는 스케줄러를 별도로 만드는 방안도 있었습니다. 하지만 날짜를 키에 포함하면 스케줄러 없이도 날짜가 바뀌는 순간 자동으로 캐시 미스가 발생해 새 GPT 응답이 생성됩니다. 유지보수할 코드를 줄이고 동작을 예측하기 쉽게 만들기 위해 이 방식을 선택했습니다.