Spring, Spring Boot/Cache - Redis

Redis Cahce - Read Through 전략

너지살 2023. 9. 7. 02:01

Spring Boot와 Redis

 

 

개요

 

Read Through 전략은 메소드에 @Cacheable을 걸어서 자동으로 Redis에 캐싱을 하는 전략 입니다.

캐시 미들웨어가 직접 데이터 소스와 연동하여 캐시 누락시 자동으로 데이터를 로드 합니다.

 

즉, 자동화된 캐싱 전략이라 생각하면 될 거 같습니다. 

 

이번에는 Read Through Cache를 기록하려 합니다. 

 

 

흐름

  1. API 요청 수신
  2. 캐시 확인
  3. 데이터가 존재할 경우 : 캐시된 값을 반환
  4. 데이터가 존재하지 않을 경우 : 캐시 미들웨어가 DB에서 데이터를 가져와 캐시에 저장한 후 값을 반환

전에 정리한 RedisTemplate, RedisHash를 이용해서 수동으로 Redis에 캐싱을 했습니다.

이러한 전략을 Cache-Aside 혹은 Lazy-Loading 이라 합니다.

Read Through 전략도 흐름은 이들과 같지만 자동화가 된다는 차이점이 있습니다. 

 

 

구현 

 

1. build.gradle에 의존성 추가

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// 직렬화, 역직렬화
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'

// cache
implementation 'org.springframework.boot:spring-boot-starter-cache'

 

 

2. application.yml에 연결 정보 및 캐시 정보 추가

spring:
	cache:
	    type: redis
	  redis:
	    host: localhost
	    port: 6379
	    password: ssafy

 

 

 

3. CacheConfig 파일 생성 

 

CacheConfig에서는 JSON 직렬화 설정, Redis 연결, 캐시 유지 시간을 설정합니다.

 

package com.dotd.product.config;


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class CacheConfig {


    // JSON 직렬화
    @Bean
    public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();

        // JSR310 모듈 등록
        om.registerModule(new JavaTimeModule());
        om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // ISO 형식으로 날짜 출력

        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(om);
        return serializer;
    }
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .entryTtl(Duration.ofMinutes(10)); // 예: 10분 동안 캐시 유지

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration).build();
    }


}

 

 

4. Redis에 저장될 ResponseDto 생성 

 

여기서는 사용한 ProductResponseDto 로 Product 객체를 DB에서 조회하면 사용자에게 전달하는 DTO 입니다.

기본적으로 Product의 Entity, Repository를 JPA로 만들어진 상태입니다.

 

ResponseDto에서는 Redis에 저장되기 위해 직렬화 설정을 추가하는 것이 중요합니다. 

implements Serializable 이 코드로 직렬화를 구현합니다. 

private static final long serialVersionUID = 1L;  버전을 설정합니다.

 

package com.dotd.product.dto;


import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.format.annotation.DateTimeFormat;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.io.Serializable;
import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductResponseDto implements Serializable {

    // 직렬화 버전
    private static final long serialVersionUID = 1L;


    private Integer id;
    private String sellerId;
    private Integer categoryId;
    private String code;
    private String name;
    private Integer price;
    private String description;
    private Integer viewCount = 0;
    private Integer orderCount = 0;
    private Integer likeCount = 0;
    private Integer reviewCount = 0;
    private LocalDateTime createdAt;


}

 

 

5. service 단에서 Read Through 메소드 생성 

 

@Cacheable 에노테이션을 통해 메소드를 캐시에 저장하게 합니다.

value와 key를 조합하여 Redis에 value::key 형태로 저장되게 됩니다.

이 때, 메소드의 return 타입이 Redis에 저장됩니다.

 

// 상품 상세 조회 Read-Through
// @Cacheable은 return을 캐시로 저장한다.
@Override
@Cacheable(value = "productResponseDto", key = "#id")
public ProductResponseDto findByIdReadThrough(Integer id) {
    Product product = productRepository.findById(id).orElse(null);
    ProductResponseDto dto = productMapper.productToProductResponseDto(product);
    return dto;
}

 

 

6. controller 생성 

 

controller를 통해 Read Through 메소드를 실행시킵니다.

 

@GetMapping("/findByIdReadThrough")
public ResponseEntity<?> findByIdReadThrough(@RequestParam(name = "id") Integer id) {
    ProductResponseDto result = productService.findByIdReadThrough(id);
    return ResponseEntity.ok(result);
}

 

 

7. 실행 결과

 

RedisInsight 프로그램으로 Redis에 저장된 것을 확인했습니다. 

 

value::key 의 형태인 productResponseDto::5 형태로 저장되었습니다.

또한 CacheConfig에서 설정한대로 JSON 형태로 저장된 것을 확인할 수 있었습니다.

 

 

 

성능체크

2023-09-07 10:45:34.628  INFO 16032 --- [nio-8080-exec-2] com.dotd.product.aop.LoggingAspect       : 위치 : ProductController.findById(..) / 경과 시간 : 98ms
2023-09-07 10:45:40.069  INFO 16032 --- [nio-8080-exec-3] com.dotd.product.aop.LoggingAspect       : 위치 : ProductController.findByIdReadThrough(..) / 경과 시간 : 412ms
2023-09-07 10:45:41.879  INFO 16032 --- [nio-8080-exec-4] com.dotd.product.aop.LoggingAspect       : 위치 : ProductController.findByIdReadThrough(..) / 경과 시간 : 31ms

 

1. MySQL에 접근해 조회한 결과 : 98ms 

2. Product를 조회할 때 Cache가 없어 Cache에 저장한 후 조회한 결과 : 412ms

3. Product를 조회할 때 Cache가 있어 Cache를 조회한 결과 : 31ms 

 

 

Redis에 캐싱된 정보를 가져올 때 조회 속도가 빨라진 것을 확인할 수 있었습니다.

 

 

 

마무리

이번에는 자동화 방법으로 조회할 때 캐싱을 넣었습니다.

다음에는 데이터를 쓰거나 변경할 때 캐싱을 업데이트해보려 합니다.

또한, Spring Boot 성능 테스트 도구를 활용하여 Redis 캐시를 사용했을 때와 안 사용했을 때와 차이를 비교해 보도록 하겠습니다.