상품 목록·상세 API는 트래픽이 높아 동일 요청이 반복되는데, 매번 DB를 조회하면 부하가 빠르게 누적됩니다.
반대로 TTL을 길게 잡으면 변경사항 반영이 늦어 오래된 데이터(stale data) 노출 위험이 커집니다.
CacheConfig에 캐시 영역을 분리하고, 조회/변경 경로에 @Cacheable / @CacheEvict를 적용했습니다.
🗂️ 캐시 정책
| 캐시 키 | TTL | 적용 대상 | 설계 의도 |
|---|---|---|---|
product:list | 30초 | 목록 조회 | 드랍 상태 전이 주기(30초)와 동기화 |
product:detail | 60초 | 상세 조회 | 목록 대비 변경 빈도가 낮아 TTL 확대 |
⚙️ 적용 포인트
status, sort, page, size)은 캐시 hit 시 DB를 우회@CacheEvict로 관련 캐시 즉시 제거장기 TTL 대신 짧은 TTL + 명시적 무효화를 채택해,
"성능"과 "최신성" 사이 트레이드오프를 운영 가능한 수준으로 균형화했습니다.
상품 상세 조회 시 연관 이미지(ProductImage)가 지연 로딩으로 개별 조회되어 N+1 쿼리 위험이 있었습니다.
ProductRepository 상세 조회 메서드에 @EntityGraph(attributePaths = "images")를 적용했습니다.
@EntityGraph(attributePaths = "images")
@Query("select p from Product p where p.id = :id")
Optional<Product> findDetailById(@Param("id") Long id);
상세 화면에 반드시 필요한 연관 데이터는 조회 시점에 명시적으로 함께 로드해
지연 로딩의 편의성보다 예측 가능한 성능을 우선했습니다.
기본 JPA 정렬만으로는 "드랍 임박순" 처럼 서브쿼리가 필요한 복합 정렬을 표현하기 어렵고,
페이지 조회마다 count 쿼리를 과도하게 실행할 수 있습니다.
ProductRepositoryCustomImpl에서 Querydsl 커스텀 조회를 적용했습니다.
⚙️ 정렬 타입
| 정렬 타입 | 기준 |
|---|---|
LATEST | 📅 등록일 내림차순 |
PRICE_HIGH | 💰 판매가 내림차순 |
PRICE_LOW | 💸 판매가 오름차순 |
DROP_IMMINENT | ⏳ 다음 드랍 시작시간 오름차순 + NULL 후순위 |
🔑 핵심 구현
JPAExpressions로 다음 드랍 시작시간 서브쿼리 계산CASE WHEN 기반 null rank 처리PageableExecutionUtils.getPage()로 count 쿼리 최적화단순 구현 편의보다 "비즈니스 정렬 정확도 + 성능" 을 위해
Querydsl 커스텀 조회를 표준 목록 경로로 채택했습니다.
| 항목 | 적용 기술 | 주요 효과 |
|---|---|---|
| 🔍 목록·상세 조회 | Redis 캐시 (@Cacheable + @CacheEvict) | 반복 요청 DB 부하 감소 + 최신성 확보 |
| 🖼️ 상세 이미지 로딩 | @EntityGraph | N+1 쿼리 제거 |
| 🔀 목록 정렬·페이징 | Querydsl 서브쿼리 + PageableExecutionUtils | 복합 정렬 구현 및 count 쿼리 최적화 |