22
33import com .fasterxml .jackson .core .type .TypeReference ;
44import com .fasterxml .jackson .databind .ObjectMapper ;
5+ import com .loopers .domain .product .ProductDetail ;
56import lombok .RequiredArgsConstructor ;
67import org .springframework .data .redis .core .RedisTemplate ;
78import org .springframework .stereotype .Service ;
89
910import java .time .Duration ;
11+ import java .util .List ;
12+ import java .util .concurrent .ConcurrentHashMap ;
13+ import java .util .stream .Collectors ;
1014
1115/**
1216 * 상품 조회 결과를 Redis에 캐시하는 서비스.
@@ -33,13 +37,25 @@ public class ProductCacheService {
3337
3438 private final RedisTemplate <String , String > redisTemplate ;
3539 private final ObjectMapper objectMapper ;
40+
41+ /**
42+ * 로컬 캐시: 상품별 좋아요 수 델타 (productId -> likeCount delta)
43+ * <p>
44+ * 좋아요 추가/취소 시 델타를 저장하고, 캐시 조회 시 델타를 적용하여 반환합니다.
45+ * 배치 집계 후에는 초기화됩니다.
46+ * </p>
47+ */
48+ private final ConcurrentHashMap <Long , Long > likeCountDeltaCache = new ConcurrentHashMap <>();
3649
3750 /**
3851 * 상품 목록 조회 결과를 캐시에서 조회합니다.
3952 * <p>
4053 * 페이지 번호와 관계없이 캐시를 확인하고, 캐시에 있으면 반환합니다.
4154 * 캐시에 없으면 null을 반환하여 DB 조회를 유도합니다.
4255 * </p>
56+ * <p>
57+ * 로컬 캐시의 좋아요 수 델타를 적용하여 반환합니다.
58+ * </p>
4359 *
4460 * @param brandId 브랜드 ID (null이면 전체)
4561 * @param sort 정렬 기준
@@ -55,7 +71,11 @@ public ProductInfoList getCachedProductList(Long brandId, String sort, int page,
5571 if (cachedValue == null ) {
5672 return null ;
5773 }
58- return objectMapper .readValue (cachedValue , new TypeReference <ProductInfoList >() {});
74+
75+ ProductInfoList cachedList = objectMapper .readValue (cachedValue , new TypeReference <ProductInfoList >() {});
76+
77+ // 로컬 캐시의 좋아요 수 델타 적용
78+ return applyLikeCountDelta (cachedList );
5979 } catch (Exception e ) {
6080 return null ;
6181 }
@@ -90,6 +110,9 @@ public void cacheProductList(Long brandId, String sort, int page, int size, Prod
90110
91111 /**
92112 * 상품 상세 조회 결과를 캐시에서 조회합니다.
113+ * <p>
114+ * 로컬 캐시의 좋아요 수 델타를 적용하여 반환합니다.
115+ * </p>
93116 *
94117 * @param productId 상품 ID
95118 * @return 캐시된 상품 정보 (없으면 null)
@@ -102,7 +125,11 @@ public ProductInfo getCachedProduct(Long productId) {
102125 if (cachedValue == null ) {
103126 return null ;
104127 }
105- return objectMapper .readValue (cachedValue , new TypeReference <ProductInfo >() {});
128+
129+ ProductInfo cachedInfo = objectMapper .readValue (cachedValue , new TypeReference <ProductInfo >() {});
130+
131+ // 로컬 캐시의 좋아요 수 델타 적용
132+ return applyLikeCountDelta (cachedInfo );
106133 } catch (Exception e ) {
107134 return null ;
108135 }
@@ -150,5 +177,96 @@ private String buildListCacheKey(Long brandId, String sort, int page, int size)
150177 private String buildDetailCacheKey (Long productId ) {
151178 return CACHE_KEY_PREFIX_DETAIL + productId ;
152179 }
180+
181+ /**
182+ * 좋아요 수 델타를 증가시킵니다.
183+ * <p>
184+ * 좋아요 추가 시 호출됩니다.
185+ * </p>
186+ *
187+ * @param productId 상품 ID
188+ */
189+ public void incrementLikeCountDelta (Long productId ) {
190+ likeCountDeltaCache .merge (productId , 1L , Long ::sum );
191+ }
192+
193+ /**
194+ * 좋아요 수 델타를 감소시킵니다.
195+ * <p>
196+ * 좋아요 취소 시 호출됩니다.
197+ * </p>
198+ *
199+ * @param productId 상품 ID
200+ */
201+ public void decrementLikeCountDelta (Long productId ) {
202+ likeCountDeltaCache .merge (productId , -1L , Long ::sum );
203+ }
204+
205+ /**
206+ * 모든 좋아요 수 델타를 초기화합니다.
207+ * <p>
208+ * 배치 집계 후 호출됩니다.
209+ * </p>
210+ */
211+ public void clearAllLikeCountDelta () {
212+ likeCountDeltaCache .clear ();
213+ }
214+
215+ /**
216+ * 상품 목록에 좋아요 수 델타를 적용합니다.
217+ * <p>
218+ * DB에서 직접 조회한 결과에도 델타를 적용하기 위해 public으로 제공합니다.
219+ * </p>
220+ *
221+ * @param productInfoList 상품 목록
222+ * @return 델타가 적용된 상품 목록
223+ */
224+ public ProductInfoList applyLikeCountDelta (ProductInfoList productInfoList ) {
225+ if (likeCountDeltaCache .isEmpty ()) {
226+ return productInfoList ;
227+ }
228+
229+ List <ProductInfo > updatedProducts = productInfoList .products ().stream ()
230+ .map (this ::applyLikeCountDelta )
231+ .collect (Collectors .toList ());
232+
233+ return new ProductInfoList (
234+ updatedProducts ,
235+ productInfoList .totalCount (),
236+ productInfoList .page (),
237+ productInfoList .size ()
238+ );
239+ }
240+
241+ /**
242+ * 상품 정보에 좋아요 수 델타를 적용합니다.
243+ * <p>
244+ * DB에서 직접 조회한 결과에도 델타를 적용하기 위해 public으로 제공합니다.
245+ * </p>
246+ *
247+ * @param productInfo 상품 정보
248+ * @return 델타가 적용된 상품 정보
249+ */
250+ public ProductInfo applyLikeCountDelta (ProductInfo productInfo ) {
251+ Long delta = likeCountDeltaCache .get (productInfo .productDetail ().getId ());
252+ if (delta == null || delta == 0 ) {
253+ return productInfo ;
254+ }
255+
256+ ProductDetail originalDetail = productInfo .productDetail ();
257+ Long updatedLikesCount = originalDetail .getLikesCount () + delta ;
258+
259+ ProductDetail updatedDetail = ProductDetail .of (
260+ originalDetail .getId (),
261+ originalDetail .getName (),
262+ originalDetail .getPrice (),
263+ originalDetail .getStock (),
264+ originalDetail .getBrandId (),
265+ originalDetail .getBrandName (),
266+ updatedLikesCount
267+ );
268+
269+ return new ProductInfo (updatedDetail );
270+ }
153271}
154272
0 commit comments