Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public ProductViewResponse getProduct(UUID productId) {
}

// 내부 통신용 옵션 유효성 검증 로직
@Cacheable(
value = "product_validate_cache",
key = "#optionId",
cacheManager = "catalogCacheManager"
)
public OptionValidationResponse validateOptionInternal(UUID optionId) {
// 1. 해당 옵션을 포함하는 상품 뷰 조회 (없으면 예외)
ProductView productView = productViewRepository.findByOptionId(optionId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.michelet.catalog.presentation.dto.OptionValidationResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
Expand All @@ -14,6 +16,7 @@
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

Expand All @@ -23,41 +26,54 @@ public class RedisCacheConfig {

@Bean
public CacheManager catalogCacheManager(RedisConnectionFactory connectionFactory) {
// 역직렬화 시 날짜 포맷 깨짐 방지 및 다형성 타입 보존을 위한 커스텀 ObjectMapper
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());

// 무방비 역직렬화 공격 차단을 위한 화이트리스트 보안 필터 적용
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
.allowIfSubType("com.michelet.catalog")
.allowIfSubType("org.springframework.data.domain")
.allowIfSubType("java.util")
.allowIfSubType("java.time")
.allowIfSubType("java.math")
.allowIfBaseType(Object.class)
.build();

objectMapper.activateDefaultTyping(
ptv,
// 다형성 타입이 필요한 캐시용 (products_cache 등)
ObjectMapper polymorphicMapper = new ObjectMapper();
polymorphicMapper.registerModule(new JavaTimeModule());
polymorphicMapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfSubType("com.michelet")
.allowIfSubType("org.springframework.data.domain")
.allowIfSubType("java.util") // 컬렉션 타입 허용 필수
.allowIfSubType("java.time") // LocalDateTime 등 날짜 타입
.allowIfSubType("java.math") // BigDecimal 등
.build(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
GenericJackson2JsonRedisSerializer polymorphicSerializer =
new GenericJackson2JsonRedisSerializer(polymorphicMapper);

GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
// 타입 정보 불필요한 단순 DTO용 (product_validate_cache)
// OptionValidationResponse는 UUID/String/BigDecimal만 있어 @class 불필요
Jackson2JsonRedisSerializer<OptionValidationResponse> validateSerializer =
new Jackson2JsonRedisSerializer<>(OptionValidationResponse.class);

RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 캐시 유효 시간 설정 (기본 1시간)
// 기본 설정 (products_cache 등에 적용)
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
// 캐시 Key는 String으로 직렬화
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// 캐시 Value는 JSON 구조로 직렬화하여 가독성 및 호환성 보장
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
// 몽고DB의 데이터가 null일 경우 캐싱을 방지 (Cache Penetration 방어)
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(polymorphicSerializer))
.disableCachingNullValues();

// product_validate_cache 전용 설정
RedisCacheConfiguration validateConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(validateSerializer))
.disableCachingNullValues();

Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("product_validate_cache", validateConfig);

return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(connectionFactory)
.cacheDefaults(config)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Caching;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

Expand All @@ -33,7 +34,12 @@ public void consumeProductCreatedEvent(ProductCreatedEvent event) {
topics = "${catalog.kafka.topic.product-updated:product.updated}",
groupId = "${spring.kafka.consumer.group-id:catalog-service-consumer}"
)
@CacheEvict(value = "products_cache", allEntries = true, cacheManager = "catalogCacheManager")
@Caching(
evict = {
@CacheEvict(value = "products_cache", allEntries = true, cacheManager = "catalogCacheManager"),
@CacheEvict(value = "product_validate_cache", allEntries = true, cacheManager = "catalogCacheManager")
}
)
// 이벤트 유입 즉시 메인 화면 캐시 제거
public void consumeProductUpdatedEvent(ProductUpdatedEvent event) {
log.info("product.updated 이벤트 수신: {}", event);
Expand All @@ -45,13 +51,15 @@ public void consumeProductUpdatedEvent(ProductUpdatedEvent event) {
topics = "${catalog.kafka.topic.status-changed:product.status-changed}",
groupId = "${spring.kafka.consumer.group-id:catalog-service-consumer}"
)
@CacheEvict(
value = "products_cache",
allEntries = true,
condition = "#event.newStatus() != 'SOLDOUT'",
cacheManager = "catalogCacheManager"
@Caching(
evict = {
// SOLDOUT 포함 모든 상태 변경 시에만 products_cache evict (목록 캐시는 조건부 유지)
@CacheEvict(value = "products_cache", allEntries = true, condition = "#event.newStatus() != 'SOLDOUT'", cacheManager = "catalogCacheManager"),
// 상태가 SOLDOUT으로 바뀌어도 단건 상세 캐시는 항상 evict (stale 옵션 검증 방지)
@CacheEvict(value = "product_validate_cache", allEntries = true, cacheManager = "catalogCacheManager")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)
// SOLDOUT 이외 상태 변경 이벤트 유입 시 메인 화면 캐시 제거
// SOLDOUT 이외 상태 변경 이벤트 유입 시 메인 화면 캐시 제거 / 상세 캐시는 항상 제거
public void consumeProductStatusChangedEvent(ProductStatusChangedEvent event) {
log.info("product.status-changed 이벤트 수신: {}", event);
productViewCommandService.updateProductStatus(event);
Expand Down
Loading