상품 목록과 상세 조회를 최적화하기 위해 캐시 구조를 설계하면서 겪은 고민과 시행착오를 정리해보았습니다. 왜 이렇게 설계했는지, 어떤 문제를 만났는지를 공유합니다.
목록 조회는 자주 발생하지만, 일부 조건(최신순, 좋아요순)에 따라 데이터 변경 빈도가 달라지므로 캐시 전략을 달리할 필요가 있었습니다.
처음에는 상품 목록 조회 시, 상품 상세(Product) + 재고(Stock) + 좋아요수(Like)를 조회 시점에 조합하는 방식으로 개발했습니다.
조회/수정이 빈번한 좋아요 수로 인한 상품 조회/수정시 성능 이슈가 걱정 되어서 였습니다. 대신 캐시 설계시 비정규화 테이블을 생성하기로 결정했습니다.
ProductListView 생성 ProductStock) 요약: 목록 조회는 비정규화 테이블, 상세 조회는 Product + Stock 조합
처음 구현에서는 목록 캐시와 상세 캐시를 각각 생성되어있고, 총 좋아요 수 가 각 캐시에 저징되어 있어, 좋아요 수정시 목록/상세 캐시가 모두 업데이트 되어야만 하는방식이었습니다. 때문에 상품과 좋아요 캐시를 우선 분리하고, 목록 캐시에서 상세 캐시를 조합하는 방식으로 진행했습니다.
// 초기 캐시 구조
목록 캐시: product:list:{brandId}:{sort}:{page}:{size}
상세 캐시: product:detail:{productId}
// 목록 조회 시 상세 캐시 조합 예시
List<ProductWithLikeCount> list = productIds.stream()
.map(id -> {
ProductStock stock = cacheRepository.get(id);
if (stock == null) stock = getProductStock(id);
LikeInfo like = likeCacheRepository.getLikeInfo(userId, id);
return new ProductWithLikeCount(
id,
stock.product().getName(),
stock.product().getPrice().getAmount(),
like.likeCount()
);
}).toList();
* 장점:
1. 상세 캐시 미스가 있어도 전체 목록을 DB에서 재조회하지 않음
2. 좋아요수 변경 시 페이지별 캐시를 일일이 삭제할 필요 없음
3. 캐시 효율과 데이터 최신성을 모두 고려 가능
List<Long> missIds = productIds.stream()
.filter(id -> cacheRepository.get(id) == null)
.toList();
if (!missIds.isEmpty()) {
List<ProductStock> stocks = missIds.stream()
.map(id -> getProductStock(id))
.toList();
stocks.forEach(stock -> cacheRepository.save(stock, DETAIL_TTL));
}
각 목록별 특징을 고려해 페이지 마다 다른 TTL 전략을 고려했습니다.
최신순(latest):
신규 데이터 추가 시 1페이지는 즉시 변경됨 → 1분
뒤로 갈수록 변화 영향이 매우 적음 → TTL 증가 가능
좋아요순(likes_desc):
좋아요는 실시간으로 변하지만 1페이지만 영향 큼
뒤쪽 페이지는 순위 변동률이 낮음 → 2분 유지
가격순, 기본
* 데이터 변경 자체가 거의 없음 → TTL 길게(10분 이상)
private Duration getListTtl(String sort, int page) {
if ("latest".equals(sort)) {
if (page == 0) return Duration.ofMinutes(1);
if (page <= 4) return Duration.ofMinutes(2);
return Duration.ofMinutes(5);
} else if ("likes_desc".equals(sort)) {
if (page == 0) return Duration.ofMinutes(1);
return Duration.ofMinutes(2);
}
return Duration.ofMinutes(10);
}
사용자별 좋아요 여부: product:liked:{userId}:{productId}
상품별 좋아요 수: product:likeCount:{productId}
이번 설계를 통해, 읽기 최적화 + 캐시 효율 + 변경 데이터 처리 문제를 모두 고려해볼수 있었습니다.