현재 프로젝트는 spring3.x를 사용하고 있어 해당 버전에 맞게 redis를 적용하여 속도개선을 했다. 이 과정에서 다양한 이슈도 생겨서 이를 기록해보고자 한다.
현재 Multi module로 구성되어 있기에 Redis 모듈도 별도로 분리하여 구성해보았다.
해당 설정은 Redis 모듈의 build.gradle 설정이다.
plugins {
id 'io.spring.dependency-management' version '1.1.2'
id 'java'
}
dependencies {
// Redis
implementation 'org.springframework.data:spring-data-redis:3.1.5'
implementation 'org.springframework.boot:spring-boot-starter-cache:3.1.2'
// 직렬화 관련
api "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1"
api 'com.fasterxml.jackson.core:jackson-databind:2.13.1'
//ReadFrom
implementation 'io.lettuce:lettuce-core:6.2.6.RELEASE'
// @ConfigurationProperties
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:3.1.2"
// aop 관련
implementation 'org.springframework.boot:spring-boot-starter-aop:2.3.1.RELEASE'
}
tasks {
processResources {
duplicatesStrategy = org.gradle.api.file.DuplicatesStrategy.INCLUDE
}
}
dependencyManagement {
imports {
mavenBom 'org.springframework.cloud:spring-cloud-dependencies:2021.0.1'
}
}
spring:
data:
redis:
cluster:
nodes: 정보~
max-redirects: 5
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.mco.domain.RedisInfo;
import io.lettuce.core.ReadFrom;
import java.util.HashSet;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
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.RedisClusterConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.StringUtils;
@Slf4j
@EnableCaching
@Configuration
@RequiredArgsConstructor
public class RedisConfig implements CachingConfigurer {
private final RedisInfo redisInfo;
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
// ip,port 등 서버 구성 관련 속성
Set<String> nodes = new HashSet<String>();
for (String node : StringUtils.commaDelimitedListToStringArray(redisInfo.getCluster().getNodes())) {
try {
nodes.add(node.trim());
} catch (RuntimeException ex) {
throw new IllegalStateException("Invalid redis sentinel" + "property" + node + "",ex);
}
}
RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration(nodes);
// clientname,readfrom등 클라이언트 관련 속성
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED)
.build();
return new LettuceConnectionFactory(clusterConfiguration,clientConfiguration);
}
//JSON 직렬화/역직렬화 관련
private ObjectMapper objectMapper() {
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator
.builder()
.allowIfSubType(Object.class)
.build();
return new ObjectMapper()
.findAndRegisterModules()
.enable(SerializationFeature.INDENT_OUTPUT)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false)
.registerModule(new JavaTimeModule())
.activateDefaultTyping(ptv, DefaultTyping.NON_FINAL);
}
@Bean
public RedisTemplate<String,Object> redisTemplate() {
final RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper()));
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(RedisSerializer.java());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.computePrefixWith(cacheName -> cacheName.concat(":"))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer(objectMapper())
));
}
@Override
@Bean
public CacheManager cacheManager() {
return RedisCacheManager.builder(this.redisConnectionFactory())
.cacheDefaults(this.cacheConfiguration())
.build();
}
@Override
public CacheErrorHandler errorHandler() {
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
log.warn(exception.getMessage(),exception);
}
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
log.warn(exception.getMessage(),exception);
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
log.warn(exception.getMessage(),exception);
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
log.warn(exception.getMessage(),exception);
}
};
}
}
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ConfigurationProperties(prefix = "spring.data.redis")
@Configuration
public class RedisInfo {
private String nodes;
private String password;
private String readFrom;
private String clientName;
private RedisInfo cluster;
}
Caching with Spring Boot 3, Lettuce, and Redis Sentinel
Spring Boot Lettuce Connection(Cluster)
@Cacheable(value = "TEST:getMain",key = "#id")
public ReportResponse getMain(long id) {}
방안
1. 캐싱 설정파일 변경 → 클러스터 환경으로 운영중
2. Redis 서버 중지 → 다른 서비스도 같이 사용함
3. 실시간 캐싱 상태값 확인
-> AOP 사용(+Annotation) → 중복 코드 삭제
implementation 'org.springframework.boot:spring-boot-starter-aop:2.3.1.RELEASE'
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisCacheable {
}
@Component
@Aspect
@RequiredArgsConstructor
public class RedisCacheAspect {
}
@Around("@annotation(RedisCacheable)")
public Object cacheableProcess(ProceedingJoinPoint joinPoint) throws Throwable {
RedisCacheable redisCacheable = getCacheable(joinPoint);
final String cacheKey = generateKey(redisCacheable.cacheName(),joinPoint);
Object[] parameterValues = joinPoint.getArgs();
if (Boolean.TRUE.equals(redisTemplate.hasKey(cacheKey)) && !confirmBypass()) {
return redisTemplate.opsForValue().get(cacheKey);
}
final Object methodReturnValue = joinPoint.proceed();
final long cacheTTL = redisCacheable.expireTime();
if (cacheTTL < 0) {
redisTemplate.opsForValue().set(cacheKey,methodReturnValue);
} else {
redisTemplate.opsForValue().set(cacheKey,methodReturnValue,cacheTTL, TimeUnit.SECONDS);
}
return methodReturnValue;
}
private RedisCacheable getCacheable(ProceedingJoinPoint joinPoint) {
final MethodSignature signature = (MethodSignature) joinPoint.getSignature();
final Method method = signature.getMethod();
return AnnotationUtils.getAnnotation(method, RedisCacheable.class);
}
private String generateKey(String cacheName, ProceedingJoinPoint joinPoint) {
String generatedKey = StringUtils.arrayToCommaDelimitedString(joinPoint.getArgs());
return String.format("%s:%s", cacheName, generatedKey);
}
Spring Boot 캐시 만료시간 설정을 위한 Redis Cache AOP 작성
마치며
redis 캐시를 태움으로 4~5초 -> 100~200ms 으로 속도를 줄일 수 있었다.
또한 AOP를 사용해서 중복 코드를 많이 제거 할 수 있었다.