티스토리 뷰

1. cache miss가 나면,
db에서 데이터를 가져오고 / db에서 가져온 값을 리턴하고 / 가져온 값을 캐시에 저장하는 프로세스로 되도록 하려고 하였음

2.캐시에서 데이터를 가져올 때, Cache.ValueWrapper라는 타입으로 가져와야 하는 건지 몰랐음.
아래처럼 사용했음

Cache productCache = redisCacheManager.getCache("productId");
Cache.ValueWrapper valueWrapper = productCache.get(String.valueOf(productId));

 

3.처음에 리턴 타입을 product라는 엔티티 클래스를 그대로 사용함.

이 product 엔티티는 category라는 엔티티와 연관관계 매핑이 되어있었고, 지연 로딩이 걸려있었다.

이런 경우 category라는 프록시 객체는 직렬화가 되지 않아 에러가 발생한다.
fetch타입을 없애거나 dto를 통해서 내려줄 수 있는 데이터를 고르는 방법이 있다고 생각함.

그래서 dto를 통해서 리턴하는 것으로 해결하였음.

 

=======================================================================================

 

<valueWrapper 문제>

 

look - aside 전략을 적용시켜보고자 아래처럼 코드를 작성하였음.

 @Transactional(readOnly = true)
    public Product findProductInCache(Long productId) {

        Cache productCache = redisCacheManager.getCache(String.valueOf(productId));

        if (productCache != null) {

            return (Product) productCache;
        }else {
            Product product = productRepository.findById(productId).orElseThrow(
                    () -> new IllegalArgumentException("해당 상품이 존재하지 않음")
            );

            productCache.put(String.valueOf(productId), product);
            return product;
        }
    }


이렇게 하니 아래와 같은 에러가 나옴

java.lang.ClassCastException: class org.springframework.data.redis.cache.RedisCache cannot be cast to class com.project.stress_traffic_system.product.model.Product (org.springframework.data.redis.cache.RedisCache is in unnamed module of loader 'app'; com.project.stress_traffic_system.product.model.Product is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @6b82ed8c)


문제는 여기에서 productCache를 product 객체로 형변환을 할 수 없는 것이었음. (일단 추측 ㅠ)

 

다른 사람의 코드를 보면 좋겠다고 생각해서 찾다가 아래에서 이런 코드를 찾음
https://rlxuc0ppd.toastcdn.net/presentation/%5BNHN%20FORWARD%202020%5D%EC%BA%90%EC%8B%9C%20%EC%84%B1%EB%8A%A5%20%ED%96%A5%EC%83%81%EC%9D%84%20%EC%9C%84%ED%95%9C%20%EC%8B%9C%EB%8F%84.pdf

 

@Override
public CacheData cacheResourceGetter(Object cacheKey) {
Cache cache = cacheManager.getCache("localCache");
Cache.ValueWrapper valueWrapper = cache.get(key);
return (CacheData)valueWrapper.get();

 

밑의 링크도 참고해보니 다들 이런 타입을 이용해서 엔티티를 검증하는 것 같음.

Cache.ValueWrapper


https://programtalk.com/java-api-usage-examples/org.springframework.cache.Cache.ValueWrapper/

 

org.springframework.cache.Cache.ValueWrapper Example

Java code examples for org.springframework.cache.Cache.ValueWrapper. Learn how to use java api org.springframework.cache.Cache.ValueWrapper

programtalk.com

 

자료가 없어서 그냥 gpt에게 물어봄. 내가 찾아본 내용과 어느정도 맞는 것 같음. 그래서 valueWrapper를 사용함.


이런 식으로 코드를 수정하니 아까 에러가 나지 않음

@Transactional(readOnly = true)
public Product findProductInCache(Long productId) {
    Cache productCache = redisCacheManager.getCache(String.valueOf(productId));
    Cache.ValueWrapper valueWrapper = productCache.get(String.valueOf(productId));
    
    if (valueWrapper != null) {
        // 캐시에서 데이터가 존재하는 경우
        return (Product) valueWrapper.get();
    } else {
        // 캐시에서 데이터가 존재하지 않는 경우
        Product product = productRepository.findById(productId).orElseThrow(
                () -> new IllegalArgumentException("해당 상품이 존재하지 않음")
        );

        productCache.put(String.valueOf(productId), product);
        return product;
    }
}

 

 

<직렬화 문제 - 1>

이젠 이런 에러가 response에 찍힘

"errorMessage": "DefaultSerializer requires a Serializable payload but received an object of type [com.project.stress_traffic_system.product.model.Product]",
"httpStatus": "BAD_REQUEST"

 

직렬화와 관련된 문제였음.

구글 검색을 통해 Product 엔티티에  Serializable을 implements 해주면 해결된다는 것을 확인함. 
해결도 됨.

그런데 gpt한테도 물어봤음.

 

이걸 보니 redis config 클래스를 만들때 이것과 비슷한 코드를 쓰던 게 생각남.
이걸 보고 관련 코드를 추가하는 방식으로 config를 수정함

<추가된 코드>

.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

 

<전체 config>

//캐시에서 Redis를 사용하기 위해 설정
    //RedisCacheManager를 Bean으로 등록하면 기본 CacheManager를 RedisCacheManager로 사용함.
    @Bean(name = "cacheManager")
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        //cache 사용을 위해 redisCacheConfiguration 초기화
        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                // null value 캐시안함
                .disableCachingNullValues()
                // 캐시의 기본 유효시간 설정 -> CacheKey의 DEFAULT_EXPIRE_SEC로 설정
                .entryTtl(Duration.ofSeconds(CacheKey.DEFAULT_EXPIRE_SEC))
                //키 앞에 붙는 prefix 형식 지정. 여기서는 '이름::' 이렇게 되도록 설정되어 있음
                .computePrefixWith(CacheKeyPrefix.simple())
                // redis 캐시 데이터 저장방식을 StringSeriallizer로 지정
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                //value 직렬화 기본 옵션이 jdk 직렬화임. 우리 엔티티 클래스들을 json형식으로 직렬화 하려면 이 설정을 꼭 넣어줘야 함.
                //아니면 이 에러가 나옴. product 엔티티 클래스를 예시로 들어봄
                // "errorMessage": "DefaultSerializer requires a Serializable payload but received an object of type [com.project.stress_traffic_system.product.model.Product]"
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        // 캐시키별 유효시간 설정
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        cacheConfigurations.put(CacheKey.USERNAME, RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(CacheKey.POST_EXPIRE_SEC)));

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory).cacheDefaults(configuration)
                .withInitialCacheConfigurations(cacheConfigurations).build();
    }

 

 

<키 이름 커스터마이징>

키가 요딴식으로 저장됨

 

보니까 내가 잘못했음.

캐시에 string.valueOf를 넣었음. "productId"로 바꿔줌

@Transactional(readOnly = true)
    public Product findProductInCache(Long productId) {
        Cache productCache = redisCacheManager.getCache(String.valueOf(productId));
        Cache.ValueWrapper valueWrapper = productCache.get(String.valueOf(productId));

        if (valueWrapper != null) {

            return (Product) productCache;
        }else {
            Product product = productRepository.findById(productId).orElseThrow(
                    () -> new IllegalArgumentException("해당 상품이 존재하지 않음")
            );

            productCache.put(String.valueOf(productId), product);
            return product;
        }
    }

 

<변경 후>

@Transactional(readOnly = true)
    public Product findProductInCache(Long productId) {
        Cache productCache = redisCacheManager.getCache("productId");
        Cache.ValueWrapper valueWrapper = productCache.get(String.valueOf(productId));

        if (valueWrapper != null) {

            return (Product) productCache;
        }else {
            Product product = productRepository.findById(productId).orElseThrow(
                    () -> new IllegalArgumentException("해당 상품이 존재하지 않음")
            );

            productCache.put(String.valueOf(productId), product);
            return product;
        }
    }

이렇게 하면 캐시의 이름은 productId, 키는 productId의 값이 됨.

 

요렇게


<직렬화 문제 - 2>

 

 

이런 에러가 나옴.

내가 그동안 테스트 했던 id가 1인 product는 category들이 null이어서 그동안 문제가 발생하지 않았었음.

값이 있는 2는 에러가 발생함.

product에 연관관계가 매핑된 category에는 지연로딩이 걸려있음.

그런데 이 프록시 객체는 직렬화가 안된다고 함.

지연로딩을 지우니 잘 작동함. 프록시 객체 문제가 맞았음.

 

지연로딩을 지울 수는 없어서 dto를 생성해서 리턴타입 등을 바꿔줌.

아래처럼 정리함.

@Transactional(readOnly = true)
    //cacheable을 사용하면 밑에 productCache.put을 안 써도 됩니다. 다만 우리의 캐싱 전략에서 언제 캐싱이 되는지 좀 더 명시적으로 아는 게 필요하다고 판단해서 사용하지 않았습니다.
//    @Cacheable("productId")
    public ProductResponseDto findProductInCache(Long productId) {
        Cache productCache = redisCacheManager.getCache("productId"); //getCache는 캐시 이름을 기준으로 캐시를 가져옴
        Cache.ValueWrapper valueWrapper = productCache.get(String.valueOf(productId)); //가져온 캐시에서 키(key)를 기준으로 캐시를 찾고, 그것의 value를 가져오기 위해 사용

        if (valueWrapper != null) {
            //
            return (ProductResponseDto) valueWrapper;
        }else {
            Product product = productRepository.findById(productId).orElseThrow(
                    () -> new IllegalArgumentException("해당 상품이 존재하지 않음")
            );

            ProductResponseDto build = ProductResponseDto.builder()
                    .id(product.getId())
                    .name(product.getName())
                    .build();

            productCache.put(String.valueOf(productId), build);
            return build;
        }
    }

 



이런 에러가 또 나옴

java.lang.ClassCastException: class org.springframework.cache.support.SimpleValueWrapper cannot be cast to class com.project.stress_traffic_system.product.model.dto.ProductResponseDto (org.springframework.cache.support.SimpleValueWrapper is in unnamed module of loader 'app'; com.project.stress_traffic_system.product.model.dto.ProductResponseDto is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @22bef31)

 

뭔지 모르겠어서 gpt한테 물어봄

 

 

아 바보

 

최종적으로 이렇게 작성됨

@Transactional(readOnly = true)
    //cacheable을 사용하면 밑에 productCache.put을 안 써도 됩니다. 다만 우리의 캐싱 전략에서 언제 캐싱이 되는지 좀 더 명시적으로 아는 게 필요하다고 판단해서 사용하지 않았습니다.
//    @Cacheable("productId")
    public ProductResponseDto findProductInCache(Long productId) {
        Cache productCache = redisCacheManager.getCache("productId"); //getCache는 캐시 이름을 기준으로 캐시를 가져옴
        Cache.ValueWrapper valueWrapper = productCache.get(String.valueOf(productId)); //가져온 캐시에서 키(key)를 기준으로 캐시를 찾고, 그것의 value를 가져오기 위해 사용

        //cache hit의 경우
        if (valueWrapper != null) {
            //value Wrapper는 래퍼 클래스니까 밸류를 get으로 가져와야 함.
            return (ProductResponseDto) valueWrapper.get();
        }else {
            //cache miss의 경우
            //db에서 데이터를 가져와서 반환
            //그리고 캐시에 저장
            Product product = productRepository.findById(productId).orElseThrow(
                    () -> new IllegalArgumentException("해당 상품이 존재하지 않음")
            );

            ProductResponseDto build = ProductResponseDto.builder()
                    .id(product.getId())
                    .name(product.getName())
                    .build();

            productCache.put(String.valueOf(productId), build);
            return build;
        }
    }
댓글