Spring boot redis cache config
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
안녕하세요. RedisTemplate 에 이어 redis cache를 알아보겠습니다.
전편 : Spring boot redis RedisTemplate에 대하여
전편에서 한 Redis 설정이 되어 있다고 가정하겠습니다.
1. 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
앞에서 의존성 추가를 이미 했으면 추가할 필요가 없습니다.
2. 캐시키 정의하기
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.time.Duration;
@RequiredArgsConstructor
@Getter
public enum RedisCacheKey {
BOOK(CacheNames.BOOK, Duration.ofMinutes(2)),
STORE(CacheNames.STORE, Duration.ofHours(1)),
;
private final String cacheName;
private final Duration expired;
public static class CacheNames {
public static final String BOOK = "book";
public static final String STORE = "store";
}
}
Book과 Store 2가지 종류를 정의했습니다.
캐시키를 생성할 때 prefix로 사용될 cacheName과 만료시간(expired)를 선언했어요.
3. 캐시키 생성기 정의하기
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
public class RedisCacheKeyGenerator implements KeyGenerator {
public static final String DELIMITER = "_";
@Override
public Object generate(Object target, Method method, Object... params) {
return StringUtils.arrayToDelimitedString(params, DELIMITER);
}
}
KeyGenerator은 정의하지 않아도, 캐시 기능은 문제없이 동작합니다. 하지만 운영하는데 있어, Redis에서 직접 데이터를 조회해야할 때 키를 만들때 다소 번거롭기 때문에 직접 정의해서 사용하면 도움이 됩니다.
실제 키가 어떻게 형성 되는지는 뒤에서 살펴 보겠습니다.
3. 레디스 캐시 설정
import com.hevia.example.redis.RedisCacheKey;
import com.hevia.example.redis.RedisCacheKeyGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import java.util.HashMap;
import java.util.Map;
@Configuration
@RequiredArgsConstructor
@EnableCaching
public class RedisCacheConfig {
private final RedisConnectionFactory redisConnectionFactory;
@Bean
public CacheManager cacheManager() {
Map<String, RedisCacheConfiguration> cacheConfigurationMap = cacheConfigurations();
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.withInitialCacheConfigurations(cacheConfigurationMap)
.build();
}
private Map<String, RedisCacheConfiguration> cacheConfigurations() {
Map<String, RedisCacheConfiguration> cacheConfigurationMap = new HashMap<>();
for (RedisCacheKey each : RedisCacheKey.values()) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(each.getExpired());
cacheConfigurationMap.put(each.getCacheName(), redisCacheConfiguration);
}
return cacheConfigurationMap;
}
/**
* 커스텀 keyGenerator 생성
*
* @return
*/
@Bean
public KeyGenerator redisCacheKeyGenerator() {
return new RedisCacheKeyGenerator();
}
}
정의된 RedisCacheKey를 기반으로 cacheManager 빈을 생성했습니다. (캐시 매니저를 2개 이상 사용하는 경우 빈의 이름을 다르게 지정할 수 있습니다.)
위의 설정에서는 데이터가 어떻게 직렬화(serialize)가 되는지 정의하지 않고 기본을 사용했습니다. 이 경우 전편에서 본 JDK 직렬화로 저장이 됩니다.
serializeValuesWith 지정 안했을 경우, 패키지 이동시 오류, 멤버 변수 추가/삭제시는 문제없습니다.import lombok.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@AllArgsConstructor
@Getter
@Builder
@ToString
@Setter
@NoArgsConstructor
public class Book implements Serializable {
private static final long serialVersionUID = -5187004375976294105L;
private Integer id;
private String title;
private LocalDateTime createdDate;
}
java.lang.IllegalArgumentException:
DefaultSerializer requires a Serializable payload but received an object of type
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
private Map<String, RedisCacheConfiguration> cacheConfigurations() {
Map<String, RedisCacheConfiguration> cacheConfigurationMap = new HashMap<>();
for (RedisCacheKey each : RedisCacheKey.values()) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(each.getExpired());
cacheConfigurationMap.put(each.getCacheName(), redisCacheConfiguration);
}
return cacheConfigurationMap;
}
serializeValuesWith 에 GenericJackson2JsonRedisSerializer 설정을 추가했습니다.
- 패키지 이동 오류 발생
- 멤버 변수 추가 오류 없음
- 멤버 변수 삭제 오류 발생
이번에는 저장은 json형태로 하고 정의한 클래스로 읽어보겠습니다.
앞서 정의한 캐시키에 타입을 추가할게요.
BOOK(CacheNames.BOOK, Duration.ofMinutes(2), Book.class),
STORE(CacheNames.STORE, Duration.ofHours(1), Store.class),
;
private final String cacheName;
private final Duration expired;
private final JavaType javaType;
RedisCacheKey(String cacheName, Duration expired, Class clazz) {
this.cacheName = cacheName;
this.expired = expired;
this.javaType = TypeFactory.defaultInstance().constructType(clazz);
}
cacheConfigurations를 아래와 같이 수정하겠습니다.
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.hevia.example.redis.RedisCacheKey;
import com.hevia.example.redis.RedisCacheKeyGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
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 java.util.HashMap;
import java.util.Map;
public static final ObjectMapper REDIS_OBJECT_MAPPER;
static {
REDIS_OBJECT_MAPPER = new ObjectMapper();
REDIS_OBJECT_MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
REDIS_OBJECT_MAPPER.registerModule(new JavaTimeModule());
REDIS_OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
private Map<String, RedisCacheConfiguration> cacheConfigurations() {
Map<String, RedisCacheConfiguration> cacheConfigurationMap = new HashMap<>();
for (RedisCacheKey each : RedisCacheKey.values()) {
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(each.getJavaType());
jackson2JsonRedisSerializer.setObjectMapper(REDIS_OBJECT_MAPPER);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(each.getExpired());
cacheConfigurationMap.put(each.getCacheName(), redisCacheConfiguration);
}
return cacheConfigurationMap;
}
위와같이 수정하게 되면 클래스 변경에 유연하게 대처가 가능합니다.
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Java 8 date/time type `java.time.LocalDateTime` not supported by default:
add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
(through reference chain: com.hevia.example.book.Book["createdDate"])
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.*;
import java.time.LocalDateTime;
@AllArgsConstructor
@Getter
@Builder
@ToString
@Setter
@NoArgsConstructor
public class Book {
private Integer id;
private String title;
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createdDate;
}
JsonSerialize, JsonDeserialize를 추가하여 해결할 수 있습니다.
4. 저장된 레디스 캐시키 확인하기
실제 키가 어떻게 생성되어 저장되는지 확인해보겠습니다.
다음코드는 무의미한 코드로 cache key가 어떻게 생성되는지 확인하기 위해 작성했습니다.
@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK)
public Book no_param() {
return new Book();
}
@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK)
public Book param1(int id) {
return new Book();
}
@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK)
public Book param2(int id, String id2) {
return new Book();
}
캐시키 생성기는 기본을 사용하여 3가지, 파라미터가 없는경우, 1개인경우, 2개이상인경우 입니다.
:: 앞부분은 RedisCacheKey에 정의된 cacheName입니다. 뒷부분은 규칙이 다양해 보이는데 이것은 기본 생성기(SimpleKeyGenerator)를 확인하면 알수 있습니다.
public class SimpleKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return generateKey(params);
}
/**
* Generate a key based on the specified parameters.
*/
public static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
}
코드를 보면 파라미터의 수에 따라 다르고 SimpleKey 객체를 사용하여 키를 생성하는것을 확인할 수 있습니다. 위의 내용을 파악하여 레디스에서 데이터를 조회해도 상관없지만 쉽게 사용하기 위해 직접 정의한 생성기를 사용해 보겠습니다.
@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK, keyGenerator = "redisCacheKeyGenerator")
public Book no_param() {
return new Book();
}
@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK, keyGenerator = "redisCacheKeyGenerator")
public Book param1(int id) {
return new Book();
}
@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK, keyGenerator = "redisCacheKeyGenerator")
public Book param2(int id, String id2) {
return new Book();
}
이전 보다는 보기 편해졌습니다.
생성기(keyGenerator)를 사용하지 않고 직접 정의할 수도 있습니다.
@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK,
key = "#book.id")
public Book get(Book book) {
return book;
}
@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK,
key = "#book.id + '_' + #book.title")
public Book get2(Book book) {
return book;
}
클래스가 파라미터로 전달되는경우 toString으로 키가 생성이 됩니다. 이보다는 key에 직접 정의하여 원하는 값으로 세팅할 수 있습니다.
5. @Cacheable과 aop
cacheable 어노테이션은 aop로 동작하게 됩니다. 따라서 동일한 클래스에서 (내부함수)호출하는 경우 어노테이션이 동작하지 않습니다. 이것은 다른 어노테이션도 동일하니 사용하는데 주의가 필요합니다.
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
댓글
댓글 쓰기