어노테이션 기반 Redis 캐싱

Posted by , June 16, 2023
Redis캐싱
Series ofRedis

RedisConfig

지난 내용과 대부분 유사한 테스트 코드를 짜봤습니다. 로컬 캐시를 Redis 기반의 글로벌 캐시로 바꿨다는점이 이번 내용의 가장 핵심이자 지난 내용과의 차이점이 될것입니다. 즉, ConcurrentHashMap 에서 RedisCache 로 전환하는 것이 이번 내용입니다.

redis 포트 설정

docker 를 이용해 redis 를 간단히 띄웠으며, 6379 포트로 지정해줬습니다.

spring.data.redis.host=localhost
spring.data.redis.password=1234
spring.data.redis.port=6379

RedisConfig 전체코드

Redis Cache를 적용하기 위해, RedisCacheManagerRedisConnectionFactory 를 등록해주면 Redis 캐시 설정이 완료됩니다. 아래 전체 코드를 일부 단위로 몇개씩 끊어서 상세히 설명해보겠습니다.

@Configuration
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String redisHost;

    @Value("${spring.data.redis.port}")
    private int redisport;

    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisport);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory());
        redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }

    @Bean
    public CacheManager redisCacheManager(){
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(connectionFactory())
                .cacheDefaults(redisCacheConfiguration)
                .build();

        return redisCacheManager;
    }
}

@Configuration 등록

@Configuration
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String redisHost;

    @Value("${spring.data.redis.port}")
    private int redisport;

우선 해당 클래스가 스프링의 구성 클래스임을 나타냈습니다. 어려운 내용은 아니므로, 자세한 설명은 생략하겠습니다.

LettuceConnectionFactory 생성

@Bean
public LettuceConnectionFactory connectionFactory() {
  return new LettuceConnectionFactory(redisHost, redisport);
}

LettuceConnectionFactory 를 생성하여 Redis와의 연결을 설정하는 빈(Bean)입니다. redisHost와 redisPort 값을 사용하여 LettuceConnectionFactory를 구성하고 반환합니다. LettuceConnectionFactory 는 Redis와의 연결을 관리하기 위한 Spring Data Redis의 연결 팩토리(Factory) 클래스로, Redis 데이터베이스와의 통신을 처리합니다.

redisTemplate 생성을 위한 Bean

@Bean
public RedisTemplate<String, Object> redisTemplate(){
  RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
  redisTemplate.setConnectionFactory(connectionFactory());
  redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
  return redisTemplate;
}

Redis 와 상호작용하기 위한 주요 클래스로써 RedisTemplate 를 활용하기위해, RedisTemplate 을 생성하는 빈을 선언했습니다. 앞서 connectionFactory( ) 메소드에서 생성한 LettuceConnectionFactory 를 RedisTemplate 와 연결하도록 했습니다. 또한, GenericJackson2JsonRedisSerializer 를 기본 직렬화기로 설정해서 객체를 JSON 형식으로 직렬화하여 Redis 에 저장하는데 사용되도록 했습니다.

RedisCacheManager 생성을 위한 Bean

@Bean
public CacheManager redisCacheManager(){
  RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(connectionFactory())
                .cacheDefaults(redisCacheConfiguration)
                .build();

        return redisCacheManager;
    }

RedisCacheManager를 생성하는 빈입니다. RedisCacheConfiguration 을 설정하고, connectionFactory() 메서드에서 생성한 LettuceConnectionFactory 와 연결하여 RedisCacheManager 를 구성합니다. 직렬화 설정으로는 StringRedisSerializer 를 키(key) 직렬화기로,GenericJackson2JsonRedisSerializer 를 값(value) 직렬화기로 사용합니다. 이 설정은 캐시된 데이터의 키와 값을 Redis에서 직렬화하고 역직렬화하는 데 사용됩니다.

참고로 Redis 에 객체를 저장할떄 이렇게 serializer 를 통해 직렬화 해줘야하는데, 이 직렬화 방법에는 다음과 같은 방법들이 있으니 참고바랍니다.

  • Jackson2JsonRedisSerializer
  • StringRedisSerializer
  • GenericJackson2JsonRedisSerializer

스프링부트 코드

BookController

테스트를 위한 코드들은 지난번과 거의 동일하다고 헀었습니다. 여기에 새롭게 컨트롤러 하나가 추가되었습니다. 문자열 하나를 쿼리스트링으로 받고, 이에대해 Service 단에서 캐싱을 진행하는 로직입니다.

@Slf4j
@RestController
@RequestMapping("/book")
@RequiredArgsConstructor
public class BookController {
    private final BookService bookService;

    @GetMapping("/data")
    public String getData(@RequestParam String param) throws InterruptedException {
        log.info("------- call Controller");

        long start = System.currentTimeMillis();
        String data = bookService.getBookById(param);
        long end = System.currentTimeMillis();

        log.info("------ controller time = {}", end - start);
        return data;
    }
}

BookService

서비스단의 코드는 지난번과 매우 흡사하나, 파라미터와 리턴값만 조금 달리해봤습니다. String 을 받고, 이에 대한 캐싱을 진행하는 방식입니다.

@Cacheable(value = "book")
    public String getBookById(String param) throws InterruptedException {
        System.out.println("Finding book" + param + " from databases...");
        Thread.sleep(5_000); // 데이터베이스 조회 쿼리가 5초 걸린다는 가정
        return param;
    }

실행결과

postman 을 통해 간단히 실행결과를 확인해봤습니다. 우선 애플리케이션 실행후 최초로 API 를 호출한 결과로, 아직 캐싱이 진행되지 않은 상태입니다. 오른쪽에 "Time" 란을 보면 86ms 라는 시간이 걸린것을 볼 수 있습니다.

다시한번 호출했을때 8ms 로 응답시간이 대폭 감소한 것을 볼 수 있습니다.