이번 포스팅에서는 Redis로 캐싱을 구현하는 방법에 대해서 알아보겠다.
현재 진행하고 있는 OMD 프로젝트에서 매번 데스크 셋업 전체를 조회하는 것은 비효율적이라는 생각이 들어 캐싱을 적용하기로 했다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
먼저 의존성을 추가하고 application.yml에 다음과 같이 작성한다.
spring:
data:
redis:
host: redis
port: 6379
ssl:
enabled: true
RedisConfig 파일을 작성하여 Redis를 사용하기 위한 설정을 정의해준다. Redis 서버와의 연결, 데이터 직렬화, 캐시 관리를 설정해주었는데 각자 상황에 맞게 작성하면 된다.
@Configuration
public class RedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory){
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(connectionFactory)
.cacheDefaults(redisCacheConfiguration)
.withInitialCacheConfigurations(redisCacheConfigurationMap)
.build();
}
}
마지막으로 캐싱을 적용해보자. 캐싱을 적용하고 싶은 메소드에 @Cacheable 어노테이션을 적용한다. 나는 page, limit, criteria 파라미터를 조합하여 캐시 키를 생성했다. 이로써 동일한 파라미터로 메서드가 호출되면 캐시된 결과를 반환하게 된다.
새로운 게시글이 생성된 후 게시글 목록 조회를 했을 경우에 최신 데이터를 반영해야 하기 때문에 @CacheEvict 적용하여 게시글이 생성될 때마다 게시글 목록 조회와 관련된 모든 캐시 항목을 제거하여 이후의 조회 요청이 최신 데이터를 반영하도록 하였다.
// 게시글 생성
@Transactional
@CacheEvict(value = "posts", allEntries = true)
public Post savePost(PostRequest request) {
// 게시글 생성 로직
return post;
}
// 게시글 목록 조회
@Transactional(readOnly = true)
@Cacheable(value = "posts", key = "#page + '-' + #limit + '-' + #criteria")
public List<PostPreviewResponse> list(Integer page, Integer limit, Integer criteria) {
// 게시글 목록 조회 로직
}
캐싱 전에는 10번의 호출을 했을 때 평균적으로 30초가 걸렸다면 캐싱 후에는 평균적으로 14초가 걸렸다. 따라서 지연 시간이 대략 50%정도 감소했다.
Could not write JSON: Java 8 date/time type java.time.LocalDateTime not supported by default: add Module
게시글 요청 응답 DTO에서 LocalDateTime
을 사용하고 있었는데, 해당 오류는 Java 8의 LocalDateTime
타입이 기본적으로 JSON 직렬화/역직렬화를 지원하지 않기 때문에 발생하는 문제였다. 이 문제를 해결하기 위해 LocalDateTime
을 지원하는 모듈을 추가해야 했다. 따라서 다음과 같은 의존성을 추가하였다. jackson-datatype-jsr310
모듈은 Java 8 날짜 및 시간 API 타입을 직렬화 및 역직렬화할 수 있도록 지원한다.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
하지만 위의 의존성을 추가해도 문제가 해결되지 않았다. 찾아보니 직렬화, 역직렬화 어노테이션을 추가해 주면 문제가 해결된다는 사례를 보고 나도 동일하게 적용해 보았다.
public class PostResponse {
...
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul")
private final LocalDateTime createdAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul")
private final LocalDateTime updatedAt;
}
위와 같이 @JsonSerialize(using = LocalDateTimeSerializer.class)
, @JsonDeserialize(using = LocalDateTimeDeserializer.class)
을 추가해주었더니 문제가 해결되었다.
직렬화 및 역직렬화 과정을 명시적으로 정의함으로써, Jackson 라이브러리가 LocalDateTime 타입의 데이터를 처리할 때 발생할 수 있는 오류를 방지하고, 원하는 형식과 타임존에 맞게 데이터를 정확하게 변환할 수 있게 되었기 때문에 오류가 해결 된 것 같다.
너무 대충 올리신거 같은데여..