SpringBoot 프로젝트에서 성능 향상을 위한 Redis Cache 적용하기

Doyeon·2023년 7월 12일
0
post-thumbnail

최근 7일간 인기있는 메뉴 3개를 조회하는 API가 있다. 인기메뉴를 조회할 때마다 DB에서 인기메뉴를 가져오도록 쿼리를 보내야 한다. 조회시 성능을 향상할 수 있는 방법으로 DB를 거치지 않고 조회가 가능하도록 인기메뉴를 캐시에 담는 방법을 사용할 것이다. 캐시는 최대 한시간 동안 유지되며, 시간대별로 인기메뉴를 첫 조회한 데이터를 캐시에 저장하고자 한다. Redis의 캐시 기능을 사용해서 인기메뉴 조회 API를 수정해보자!

설정

의존성 추가

build.gradle

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

redis 기능을 사용하기 위해 의존성을 추가한다.

application 설정

application.yml

spring:
  redis:
    host: localhost
    port: 6379

어플리케이션 설정 파일에 redis의 host, port 번호를 작성한다.

@EnableCaching

Application.java

@EnableCaching
@EnableJpaAuditing
@SpringBootApplication
public class CoffeeApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoffeeApplication.class, args);
    }

}

어플리케이션 내 캐싱을 사용하기 위해 SpringBoot Application 파일에 @EnableCaching 을 추가한다.

Redis Configuration

RedisConfig.java

@Configuration
public class RedisConfig implements CachingConfigurer {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        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<>();
        redisCacheConfigurationMap.put("popular_menus", redisCacheConfiguration.entryTtl(Duration.ofHours(1)));

        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(connectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .withInitialCacheConfigurations(redisCacheConfigurationMap)
                .build();
    }
}
  • redisConnectionFactory()
    • redis 서버와 연결할 수 있는 ConnectionFactory 를 생성한다.
    • redis 클라이언트로는 JedisLettuce 가 있다. Spring에서는 Lettuce 가 기본으로 설정되어 있다.
    • Jedis : 멀티쓰레드 환경에서 unsafe하다. Connection pool을 이용해 멀티쓰레드 환경을 구성해야 하며, Jedis 인스턴스와 연결할 때마다 Connection pool을 불러와 물리적 비용 증가가 발생한다.
    • Lettuce : 멀티쓰레드 환경에서 safe하다. netty 라이브러리 위에 구축된다. 비동기 방식으로 요청하여 TPS/CPU/Connection 개수와 응답속도 등 여러 분야에서 Jedis보다 뛰어나다.
  • redisTemplate()
    • Spring Data Redis는 RedisTemplate, RedisRepository 두 가지 방식으로 접근할 수 있다. 여기서는 RedisTemplate 을 이용해 Redis 서버에 접근한다.
    • RedisTemplate 을 이용해서 Redis 데이터베이스와 상호작용하는 데 필요한 설정을 구성한다. Redis에 문자열 키와 JSON 형식의 값으로 데이터를 저장하고 검색할 수 있다.
    • RedisTemplate : 일반적으로 Redis의 다양한 데이터 유형인 문자열, 해시, 목록, 집합, 정렬된 집합 등을 처리하는 데 사용된다. 객체들을 자동으로 직렬화/역직렬화하며 binary 데이터를 Redis에 저장한다. 각 데이터 유형에 대해 기본적인 CRUD 작업이 가능하다. 저수준의 API로 직접적인 Redis 데이터베이스 작업을 수행하는데 사용된다.
    • RedisRepository : 더 추상화된 레벨의 컴포넌트로, Redis 데이터베이스에 대한 공통 작업을 더 편리하게 처리할 수 있는 고수준의 메서드를 제공한다. Spring Data JPA의 JpaRepository와 유사한 형태로 작성되며, 엔티티 CRUD와 같은 고수준의 작업을 지원한다. 개발자가 인터페이스를 정의하고 해당 인터페이스를 구현하는 Redis 저장소 클래스를 생성하여, 직접 Redis 작업을 구현할 필요 없이 Spring Data Redis의 기능을 활용할 수 있다.
  • redisCacheManager
    • Redis Cache 적용을 위해 RedisCacheManager를 설정한다. 기본 RedisCacheConfiguration을 가져온 후, key와 value의 직렬화 방식을 설정한다. 여기서는 StringRedisSerializer 를 사용하여 문자열로 된 키를 직렬화하고, GenericJackson2JsonRedisSerializer 를 사용하여 일반적인 객체를 JSON 형식으로 직렬화한다.
    • Redis 캐시의 이름과 RedisCacheConfiguration 을 매핑하는 맵을 생성한다. 여기서는 “popular_menu”라는 이름으로 Redis 캐시를 생성했다. 인기메뉴 조회를 시간별로 캐시에 담아 사용할 것이므로 entryTtl(Duration.ofHours(1)) 로 캐시 항목의 유효기간을 1시간으로 설정한다.

캐시 적용

@Cacheable

MenuService.java

@Cacheable(cacheNames = "popular_menus", key = "#hour")
public List<PopularMenuResponseDto> getPopularMenus(int hour) {
  return menuRepository.findWeeklyTopThreeMenus();
}

캐시를 적용하고 싶은 메서드에 @Cacheable 을 선언한다. getPopularMenus 메서드를 실행하면 “popular_menus”라는 캐시 항목에 key는 조회한 시간, value에는 조회 결과 데이터로 캐시된다.

18시 10분에 인기메뉴를 조회했다면 “popular_menus” 항목에 key = 18, value = {조회 결과} 로 데이터를 캐시한다. 이후 19시 전까지 인기메뉴를 조회하면 “popular_menus” 캐시 항목에서 key = 18에 해당하는 value를 캐시에서 바로 가져온다.

캐시 확인

캐시 적용 전

캐시가 적용되기 전, 인기메뉴를 조회하면 DB에서 데이터를 가져오기 위해 쿼리가 실행되며, API 실행 속도는 503ms이다.


캐시 적용 후

캐시 적용 후, 인기메뉴를 조회하면 캐시에서 데이터를 가져오므로 DB로 쿼리를 날리지 않는다. Redis에서 key를 조회하면 “popular_menus” 캐시 항목에 key = 23으로 캐시 항목이 존재하는 것을 확인할 수 있다. 캐시에서 데이터를 가져오므로 API 실행 속도는 53ms로 빠른 속도를 보인다.


마치며

복잡도가 있는 쿼리를 사용하면서, 자주 조회하는 데이터는, 매 요청마다 DB에 쿼리를 날려 데이터를 가져오는 것보다 캐시에 저장하여 바로 결과를 가져오는 것이 성능 면에서 더 좋다.

다만, 캐시 데이터를 언제 업데이트할 것이며, 캐시 유효기간을 어떻게 설정할 지 등 캐시 정책을 상황에 알맞게 잘 세우는 것이 중요하다.

좀 더 효율적이며, 정확한 데이터 결과를 도출하기 위해 어떤 캐시 기능을 더 적용할 수 있을지 고민해봐야겠다.

profile
🔥

0개의 댓글