개요
Read Through 전략은 메소드에 @Cacheable을 걸어서 자동으로 Redis에 캐싱을 하는 전략 입니다.
캐시 미들웨어가 직접 데이터 소스와 연동하여 캐시 누락시 자동으로 데이터를 로드 합니다.
즉, 자동화된 캐싱 전략이라 생각하면 될 거 같습니다.
이번에는 Read Through Cache를 기록하려 합니다.
흐름
- API 요청 수신
- 캐시 확인
- 데이터가 존재할 경우 : 캐시된 값을 반환
- 데이터가 존재하지 않을 경우 : 캐시 미들웨어가 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 캐시를 사용했을 때와 안 사용했을 때와 차이를 비교해 보도록 하겠습니다.