[Redis] Spring 3.x Redis 적용기(AOP)

yeonjoo913·2023년 11월 21일
0

Redis

목록 보기
3/7

현재 프로젝트는 spring3.x를 사용하고 있어 해당 버전에 맞게 redis를 적용하여 속도개선을 했다. 이 과정에서 다양한 이슈도 생겨서 이를 기록해보고자 한다.

프로젝트 구조

현재 Multi module로 구성되어 있기에 Redis 모듈도 별도로 분리하여 구성해보았다.

Redis 의존성

해당 설정은 Redis 모듈의 build.gradle 설정이다.

  • spring3.x 이상은 spring-data-redis 3.1.5 추천
  • spring-data-redis : spring data redis 및 lettuce 클라이언트와 함께 redis key-value 데이터 저장소를 사용하기 위한 것이다.
  • spring-boot-starter-cache : spring framework의 캐싱 지원을 위한 스타터로, 캐싱 공급자와 작업할 때 매우 편리한 주석을 제공한다. (선택)
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'
    }
}

Redis 연동 설정

  1. application.yml
spring:
  data:
    redis:
      cluster:
        nodes: 정보~
        max-redirects: 5
  1. RedisConfig
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);
            }
        };
    }
}
  1. redisInfo
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;

}

Reference

Caching with Spring Boot 3, Lettuce, and Redis Sentinel

Spring Boot Lettuce Connection(Cluster)

테스트

  • @Cacheable로 테스트로 진행
@Cacheable(value = "TEST:getMain",key = "#id")
public ReportResponse getMain(long id) {}

Bypass Cache at Runtime

방안
1. 캐싱 설정파일 변경클러스터 환경으로 운영중
2. Redis 서버 중지다른 서비스도 같이 사용함
3. 실시간 캐싱 상태값 확인
-> AOP 사용(+Annotation) → 중복 코드 삭제

AOP 의존성

implementation 'org.springframework.boot:spring-boot-starter-aop:2.3.1.RELEASE'
  1. Target 선언하기(특정 어노테이션이 붙은 메소드에만 캐시 태울 예정)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisCacheable {

}
  1. Aespect 선언하기(어노테이션 기준으로 실행시킬 캐시 로직을 작성)
  2. PointCut 선언하기(어떤 joinpoint에서 advice 실행 시킬지 선언)
  • spring aop는 프록시 기반이기 때문에 jointpoint가 메서드 실행 시점뿐이고, pointcut도 메서드 실행 시점만 가능하다.
@Component
@Aspect
@RequiredArgsConstructor
public class RedisCacheAspect {

}
  1. Advice 구성하기
  • advice 적용 방식 - 어노테이션이 붙은 포인트에서 실행
  • target 메소드를 실행하는 부분은 jointPoint.proceed()이다.
@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);
}

Reference

Spring Boot 캐시 만료시간 설정을 위한 Redis Cache AOP 작성


마치며
redis 캐시를 태움으로 4~5초 -> 100~200ms 으로 속도를 줄일 수 있었다.
또한 AOP를 사용해서 중복 코드를 많이 제거 할 수 있었다.

profile
주니어 백엔드 개발자. 까먹는다 기록하자!

0개의 댓글