이전 포스트에서 Redis를 설치하고 Spring boot 프로젝트와 연동하는 방법을 알아보았습니다. 이번 포스트에서는 실제로 Redis를 적용하는 방법과 성능의 차이를 비교해보도록 하겠습니다.
Redis는 key:value형식으로 데이터를 다룹니다. value로 입력되는 값은 직렬화된 형태로 저장됩니다. Redis Config 파일에 직렬화/역직렬화를 위한 cacheManager 설정 부분을 추가하도록 하겠습니다.
@Bean
public CacheManager cacheManager() {
RedisCacheManager.RedisCacheManagerBuilder builder =
RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory());
RedisCacheConfiguration configuration =
RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues()
.entryTtl(Duration.ofMinutes(30L));
builder.cacheDefaults(configuration);
return builder.build();
}
Java에서는 직렬화/역직렬화를 위해 다양한 직렬화 규칙을 적용 할 수 있습니다. (StringRedisSerializer, Jackson2JsonRedisSerializer, GenericJackson2JsonRedisSerializer 등)
저는 이중 GenericJackson2JsonRedisSerializer를 사용해보겠습니다.
GenericJackson2JsonRedisSerializer의 이점은 다른 직/역직렬화 규칙과는 다르게 @class정보를 value값과 함께 등록하여 Object mapping하여 사용할 수 있다는 점입니다.
Cache적용에 앞서 캐싱하기 위한 데이터의 형태와 변경이 발생하였을 경우 대응 방안 등 캐싱 전략을 우선적으로 수립하여야합니다.(캐싱 전략에 대하여 개발자 KimJunHee님의 블로그 글)
이 포스팅은 캐싱으로 인한 조회 성능 개선 비교를 중심으로 작성하기 때문에 캐싱 전략에 대한 설명은 생략하겠습니다. Spring framework을 사용하신다면 org.springframework.cache.annotation에서 제공하는 @Cacheable, @CachePut, @CacheEvict annotation을 사용하여 간단하게 데이터를 저장하거나 수정, 조회할 수 있습니다.
메서드의 반환값을 캐시 저장소에 저장하고 조회를 위해 사용합니다. @Cacheable이 붙어있는 메소드가 실행되면 데이터를 DB에서 조회하는 것이 아닌 우선 Cache 저장소(이 경우 Redis)에 명시된 key값으로 데이터 존재 유무를 확인합니다.
만약 데이터가 존재할 경우 DB조회 단계는 생략되고 Cache 저장소의 내용을 반환하게됩니다.
데이터가 존재하지 않을 경우 DB조회 로직을 수행하게되고 반환하는 결과값에 대하여 Cache 저장소에 저장 후 결과값을 반환하게됩니다.
캐싱되어있지 않은 데이터를 조회할 경우 최초 요청에 대해서는 Redis 저장소에 데이터를 등록하는 추가적인 작업이 필요하기 때문에 단순 조회에 비해 성능에서 희생을 필요로하게되지만, 이후 요청에 대해서는 단순 조회보다 월등히 뛰어난 성능을 보여주게 됩니다.
다음은 체육관 전체 목록을 조회하는 로직입니다.
@Cacheable(cacheNames = "gyms", key = "'all'")
public Gyms findAllGymsWithCache(){
List<Gym> gyms = gymRepository.findAll();
return new Gyms(gyms);
}
체육관의 전체 목록에 대한 조회 요청이 있을 경우 우선 Redis 저장소의 gyms::all으로 저장되어있는 데이터를 조회합니다.
만약 데이터가 존재하지 않을 경우 gymRepository.findAll();
이 실행되고 결과값에 대하여 Redis 저장소에 등록, 데이터 반환 과정이 차례대로 진행됩니다.
Test 코드를 통해 위 로직을 실행하고 최초 등록 시 응답에 걸리는 시간과 Redis 저장소에 등록 내용을 확인해보겠습니다.
@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class 체육관_조회_테스트{
@Autowired
GymService service;
@Autowired
GymRepository repository;
@Nested
@DisplayName("체육관 조회 테스트 (Redis cache 적용)")
class 캐시_조회_테스트{
@BeforeEach
public void insert_bulk(){
int gymCount = 15000;
for(int i = 1; i <= gymCount; i++ ){
Gym gym = new Gym("test gym "+i);
repository.save(gym);
}
}
@Test
@DisplayName("등록되어있는 체육관의 목록을 Redis 저장소를 참조하여 조회한다.")
public void 체육관_전체조회_캐싱(){
long before = System.currentTimeMillis();
Gyms gyms = service.findAllGymsWithCache();
log.debug("전체 데이터 수: {}", gyms.getGyms().size());
long after = System.currentTimeMillis();
long diff = after - before;
log.debug("전체 조회 캐시 최초 실행 시간: {}", diff);
Assertions.assertEquals(gyms.getGyms().size(), 15000);
}
}
데이터베이스 성능 향상시키기 첫 포스팅에서 Gym 15000개의 데이터를 조회하는 데 797ms가 소요되었습니다.