Skip to content

Commit af777a8

Browse files
committed
feat: Redis 설정 및 캐시 기본 구조 구축
성능 최적화를 위한 Redis 캐시 레이어 기본 인프라 구축 - CacheConfig: Redis 캐시 설정 및 CacheManager 구성 - CacheKeyGenerator: 일관된 캐시 키 생성 유틸리티 - CacheInvalidationService: 캐시 무효화 로직 중앙화
1 parent 384face commit af777a8

3 files changed

Lines changed: 128 additions & 0 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.loopers.infrastructure.cache;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.fasterxml.jackson.databind.SerializationFeature;
5+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
9+
import org.springframework.data.redis.core.RedisTemplate;
10+
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
11+
import org.springframework.data.redis.serializer.StringRedisSerializer;
12+
13+
@Configuration
14+
public class CacheConfig {
15+
16+
@Bean
17+
public RedisTemplate<String, Object> cacheRedisTemplate(LettuceConnectionFactory connectionFactory) {
18+
RedisTemplate<String, Object> template = new RedisTemplate<>();
19+
template.setConnectionFactory(connectionFactory);
20+
21+
// Key serializer
22+
template.setKeySerializer(new StringRedisSerializer());
23+
template.setHashKeySerializer(new StringRedisSerializer());
24+
25+
// Value serializer - JSON
26+
ObjectMapper objectMapper = new ObjectMapper();
27+
objectMapper.registerModule(new JavaTimeModule());
28+
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
29+
30+
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);
31+
template.setValueSerializer(jsonSerializer);
32+
template.setHashValueSerializer(jsonSerializer);
33+
34+
template.afterPropertiesSet();
35+
return template;
36+
}
37+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.loopers.infrastructure.cache;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.stereotype.Component;
6+
7+
@Slf4j
8+
@RequiredArgsConstructor
9+
@Component
10+
public class CacheInvalidationService {
11+
12+
private final ProductDetailCache productDetailCache;
13+
14+
/**
15+
* Invalidate caches when a product is liked or unliked
16+
*
17+
* @param productId The product that was liked/unliked
18+
* @param brandId The brand of the product
19+
*/
20+
public void invalidateOnLikeChange(Long productId, Long brandId) {
21+
log.info("[CacheInvalidation] Invalidating caches for productId={}, brandId={}", productId, brandId);
22+
23+
// Invalidate product detail cache
24+
productDetailCache.delete(productId);
25+
26+
// Note: Product list cache는 TTL(60초)에 의존하여 자동 무효화
27+
28+
log.info("[CacheInvalidation] Cache invalidation completed for productId={}, brandId={}", productId, brandId);
29+
}
30+
31+
/**
32+
* Invalidate cache when product info is updated (name, price, description, etc.)
33+
*/
34+
public void invalidateOnProductUpdate(Long productId) {
35+
log.info("[CacheInvalidation] Invalidating cache for product update, productId={}", productId);
36+
productDetailCache.delete(productId);
37+
}
38+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.loopers.infrastructure.cache;
2+
3+
import com.loopers.domain.product.enums.ProductSortCondition;
4+
5+
public class CacheKeyGenerator {
6+
7+
private static final String VERSION = "v1";
8+
9+
// Product detail cache key
10+
public static String productDetailKey(Long productId) {
11+
return String.format("product:detail:%s:%d", VERSION, productId);
12+
}
13+
14+
// Top liked products (global) cache key
15+
public static String topLikedProductsKey() {
16+
return String.format("products:top-liked:global:%s", VERSION);
17+
}
18+
19+
// Brand top products cache key
20+
public static String brandTopProductsKey(Long brandId, int limit) {
21+
return String.format("products:top-liked:brand:%s:%d:%d", VERSION, brandId, limit);
22+
}
23+
24+
// Brand top products pattern for deletion
25+
public static String brandTopProductsPattern(Long brandId) {
26+
return String.format("products:top-liked:brand:%s:%d:*", VERSION, brandId);
27+
}
28+
29+
// Product list cache key
30+
public static String productListKey(Long brandId, ProductSortCondition sort, int page, int size) {
31+
String brandParam = brandId != null ? String.valueOf(brandId) : "all";
32+
String sortParam = sort != null ? sort.name() : "DEFAULT";
33+
return String.format("products:list:%s:brand=%s:sort=%s:page=%d:size=%d",
34+
VERSION, brandParam, sortParam, page, size);
35+
}
36+
37+
// Member likes cache key
38+
public static String memberLikesKey(Long memberId) {
39+
return String.format("likes:member:%s:%d", VERSION, memberId);
40+
// 예: "likes:member:v1:123"
41+
}
42+
43+
// Product like count cache key
44+
public static String productLikeCountKey(Long productId) {
45+
return String.format("product:like:count:%s:%d", VERSION, productId);
46+
// 예: "product:like:count:v1:456"
47+
}
48+
49+
// Product like count cache key pattern (for scanning)
50+
public static String productLikeCountKeyPattern() {
51+
return String.format("product:like:count:%s:*", VERSION);
52+
}
53+
}

0 commit comments

Comments
 (0)