Redis 를 활용해 데이터를 캐싱하여 조회 성능을 개선해보자.
캐시(Cache)
원본 저장소(Database) 보다 빠르게 가져올 수 있는 임시 데이터 저장소
캐싱(Caching)
캐시(임시 데이터 저장소)에 접근해 데이터를 빠르게 가져오는 방식
데이터를 조회할 때 Redis 를 먼저 쳐서 데이터를 조회하고, 조회하고자 하는 데이터가 저장되어 있지 않으면 DB 에서 조회해 오는 방식
데이터를 저장할 때 Redis 에 반영하지 않고 DB 에만 데이터를 저장
Cache Aside 는 조회에 초점을 맞추고, Write Around 는 저장에 초점을 맞추고 있어 두 전략을 같이 사용하는데, 이 때 문제가 될 수 있는 점이 2가지가 있다.
데이터 동기화를 위해 DB 수정 사항이 있을 때마다 Redis 까지 반영하게 되면 성능 부하가 커지게 되기 때문에, 캐싱 서버를 활용할 때 이러한 문제를 해결하기 위해서 아래와 같은 케이스의 데이터를 기준으로 활용하는 것이 좋다.
또한 Redis 의 TTL 기능을 활용해서, 적절한 주기로 데이터를 동기화할 수 있도록 한다.
데이터가 만료되면 Cache Miss 가 발생하기 때문에 새롭게 DB 에서 조회한 후에 Redis 에 저장한다.
또 자주 사용하지 않는 데이터는 TTL 에 의해 자동으로 삭제될 것이기 때문에 저장 공간에 대한 여유도 확보할 수 있다.
프로젝트 설정
- Gradle
- Java 17
- Spring Boot
3.3.5
- MySQL version
8.*
이상- Dependency
Spring Boot DevTools
Spring Web
Spring Data JPA
MySQL Driver
조회 성능을 테스트할 수 있는 간단한 조회 API 를 구현했다.
해당 API 에 캐싱을 적용할 수 있도록 Redis 설정을 추가한다.
Redis 연결을 설정한다.
spring data redis 에서 제공하는 Lettuce
라이브러리를 활용해 Redis 연결을 관리하는 Bean 을 등록한다.
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public LettuceConnectionFactory lettuceConnectionFactory() {
// Redis 서버의 호스트, 포트 설정
return new LettuceConnectionFactory(host, port);
}
}
Spring 의 캐싱 기능을 활성화하고 Redis 를 캐시 저장소로 사용하기 위한 추가 설정을 작성한다.
RedisCacheConfiguration
을 통해 캐시 저장 방식과 TTL 설정을 추가했다.
정의된 캐시 설정을 적용한 RedisCacheManager
빈을 등록해 board 도메인의 캐싱을 위한 boardCacheManager 을 생성한다.
@Configuration
@EnableCaching // Spring Boot 의 캐싱 활성화
public class RedisCacheConfig {
@Bean
public CacheManager boardCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration =
RedisCacheConfiguration
.defaultCacheConfig()
.serializeKeysWith( // Redis 에 Key 를 저장할 때 String 으로 직렬화해 저장
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()
))
.serializeValuesWith( // Redis 에 Value 를 저장할 때 Json 으로 직렬화해 저장
RedisSerializationContext.SerializationPair.fromSerializer(
new Jackson2JsonRedisSerializer<Object>(Object.class)
))
.entryTtl(Duration.ofMinutes(1)); // TTL 설정
return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
Spring 의 @Cacheable
을 사용하면 Cache Aside 전략을 적용할 수 있다.
예를 들어 1페이지에서 10개의 board 를 조회하는 요청이 오면 Redis 에 boards:page:1:size:10
key 로 저장된 데이터가 있는지 확인하고, 데이터가 없다면 내부 로직을 수행한다. Redis 에 저장되어 있을 경우 메서드를 실행하지 않고 Redis 에서 해당 value 를 바로 반환한다.
@Cacheable(cacheNames = "getBoards", key = "'boards:page:' + #page + ':size:' + #size", cacheManager = "boardCacheManager")
public List<Board> getBoards(int page, int size) {
Pageable pageable = PageRequest.of(page - 1, size);
Page<Board> boards = boardRepository.findAllByOrderByCreatedAtDesc(pageable);
return boards.getContent();
}
애플리케이션을 실행하고 첫 요청은 Redis 에 아무 데이터도 저장되어 있지 않기 때문에 아래와 같은 로그를 확인할 수 있다. Redis 에 데이터가 없어 DB select 후 캐싱되었다.
첫번째 요청에서 Cache miss 가 발생했기 때문에 두번째 요청 시에는 아래와 같은 로그를 확인할 수 있다.
Redis 에서 boards:page:1:size:10
key 로 저장된 데이터를 조회했다.
Redis 에 정상적으로 데이터가 저장된 것을 확인할 수 있다.
첫번째 요청과 두번째 요청의 속도 차이를 비교해보자.
postman 으로 캐싱 전후의 수치를 확인할 수 있다.