티스토리 뷰
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/
자료가 없어서 그냥 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;
}
}
'Learned!' 카테고리의 다른 글
m1 mac brew 로 jmeter 설치했을 때 경로 (0) | 2023.03.13 |
---|---|
Ngrinder 시나리오 스크립트 빠르게 작성하고 적용 (0) | 2023.03.07 |
redis에 넣어둔 캐시 조회가 안되던 문제 (0) | 2023.02.28 |
redisson lock 설정하고 조회할 때 에러 /java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: ~~~ (0) | 2023.02.27 |
redisson에서 elasticache에 연결이 안되는 문제 /connection timed out: ~~~~~~ (0) | 2023.02.25 |
- Total
- Today
- Yesterday
- 스프링faker
- Python
- 프로그래머스
- 항해
- Redis
- pessimisticlock
- jmeter테스트
- jmeter세션
- jwt
- Lock
- index
- Redisson
- EC2
- Spring
- CheckedException
- Java
- jmeter로그인
- bankersRounding
- 동적크롤링
- CorrectnessAndTheLoopInvariant
- 대규모더미데이터
- jmeter부하테스트
- 토큰
- jmeter시나리오
- 자바
- jmeter토큰
- hackerrank
- 부하테스트시나리오
- jmeter쿠키
- 인덱스
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |