Redis Cache 기능을 활용한 성능 개선 이야기 Part 2

3 minute read


지난 part 1 포스팅 에서 캐싱을 적용하기 전 어떤 점을 알아보고 고려해봤는지 정리를 해보았습니다. 이제 실제 프로젝트에 적용을 해보고 어떻게 성능이 개선되었는지 자세히 정리해보도록 하겠습니다 :)


캐싱 적용해보기

@EnableCaching


@EnableCaching
@SpringBootApplication
public class FestaApplication {

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


맨 처음으로 어플리케이션에서 main() 메서드가 있는 SpringBootApplication 파일에 @EnableCaching을 추가하여 어플리케이션 내에 캐싱을 이용하겠다는 명시를 하도록 합니다.

@EnableCaching 을 적용하게 되면 @Cacheable 이라는 어노테이션이 명시된 메서드가 실행될 때 내부적으로 Proxy, AspectJ 기반 어드바이스를 CacheInterceptor 와 연결하여 Spring에서 캐시 관리에 필요한 구성요소로 등록을 하게 됩니다.



Redis Configuration

프로젝트 내에서 Redis Configuration을 작성한 파일에 Cache Manager 를 구현하여 캐싱 사용을 위한 설정코드를 입력합니다.


@Configuration
public class RedisConfig {

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

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


    /*
        Lettuce: Multi-Thread 에서 Thread-Safe한 Redis 클라이언트로 netty에 의해 관리된다.
                 Sentinel, Cluster, Redis data model 같은 고급 기능들을 지원하며
                 비동기 방식으로 요청하기에 TPS/CPU/Connection 개수와 응답속도 등 전 분야에서 Jedis 보다 뛰어나다.
                 스프링 부트의 기본 의존성은 현재 Lettuce로 되어있다.

        Jedis  : Multi-Thread 에서 Thread-unsafe 하며 Connection pool을 이용해 멀티쓰레드 환경을 구성한다.
                 Jedis 인스턴스와 연결할 때마다 Connection pool을 불러오고 스레드 갯수가
                 늘어난다면 시간이 상당히 소요될 수 있다.
     */
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(new RedisStandaloneConfiguration(redisHost, redisPort));
    }


    /*
        RedisTemplate: Redis data access code를 간소화 하기 위해 제공되는 클래스이다.
                       주어진 객체들을 자동으로 직렬화/역직렬화 하며 binary 데이터를 Redis에 저장한다.
                       기본설정은 JdkSerializationRedisSerializer 이다.

        StringRedisSerializer: binary 데이터로 저장되기 때문에 이를 String 으로 변환시켜주며(반대로도 가능) UTF-8 인코딩 방식을 사용한다.

        GenericJackson2JsonRedisSerializer: 객체를 json타입으로 직렬화/역직렬화를 수행한다.
     */
    @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;
    }

    /*
        Redis Cache 적용을 위한 RedisCacheManager 설정
     */
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext
                                    .SerializationPair
                                    .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext
                                    .SerializationPair
                                    .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        Map<String, RedisCacheConfiguration> cacheConfiguration = new HashMap<>();
        cacheConfiguration.put(RedisCacheKey.CATEGORY_LIST, redisCacheConfiguration.entryTtl(Duration.ofSeconds(180L)));


        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .build();
    }
}


redisCacheManager 라는 메서드 안에 Map을 이용하여 캐시 키를 등록해주었고 entryTtl은 모든 키에 대한 설정이 아닌 각각의 키 별로 지정할 수 있게 categoryList 키 하나에 한정해서 180초로 설정을 해두었습니다.



Redis Cache Key

Redis 는 데이터는 Key-Value 값으로 저장하기 때문에 키에 대한 이름을 설정해야합니다. 키의 이름은 따로 파일을 만들어 상수화 하여 사용을 했습니다.


public class RedisCacheKey {

    public static final String CATEGORY_LIST = "categoryList";
}



@Cachable

이제 설정은 모두 완료되었으니 비로소 캐싱을 적용하고 싶은 메서드에 @Cachable 을 선언하여 해당 메서드에 요청이 오면 데이터를 캐시할 수 있도록 명시를 합니다. 제 프로젝트에서는 이벤트 목록/이벤트 카테고리별 조회 기능에 추가를 했습니다 :)


@Cacheable(key = "#categoryCode", value = CATEGORY_LIST, cacheManager = "redisCacheManager")
public List<EventDTO> getListOfEvents(PageInfo pageInfo, int categoryCode) {
    return eventDAO.getListOfEvents(pageInfo, categoryCode);
}


image

아래에 다시 한번 서술하겠지만, 이벤트 목록조회/카테고리별 조회를 할 경우 위와 같이 categoryList::(실제 카테고리 코드) 라는 우리가 지정한 키의 이름으로 데이터가 캐시됩니다.



@CacheEvict

캐시 키 값은 @CacheEvict 로 지워주지 않는 한, 메모리에 계속 남아있게 됩니다. 저는 새로운 이벤트가 등록이 되면 refresh를 위해서 이벤트 등록 기능에 캐시 데이터를 삭제할 수 있도록 설정하였습니다.

내용 업데이트 기능에도 @CacheEvict 를 선언하는 경우도 많이 봤었는데, 개개인의 어플리케이션에서 많은 테스트를 거쳐 적절한 곳에 선언을 해주면 될 것 같습니다.



정말 캐싱이 되는지 확인해보자

저는 Talend API Tester 라는 chrome 확장 프로그램을 다운로드 하여 사용했습니다. 해당 툴은 REST API 나 HTTP를 보다 손쉽게 테스트 할 수 있도록 제공하는 기능으로, 프로젝트에 설정한 url 요청을 보내면 패턴에 대한 유효성을 체크함과 동시에 그에 맞는 응답을 제공해줍니다. 무료 버전으로 제공되고 있고 다운로드만 하면 바로 사용할 수 있기 때문에 REST API 테스트 할 때 추천드리고 싶은 툴 입니다.


URL


요청할 URL과 파라미터 값을 입력하여 send를 누르면


image


캐싱이 적용되기 전에는 137ms 의 속도가 출력됩니다. 현재 목록이 많은 것은 아니지만 실제 서비스에선 수 천, 수 만개의 목록을 불러들이기 때문에 이보다 몇 배 이상으로 걸릴 수 있습니다.


image

캐싱을 적용해 본 후의 속도입니다. 이전엔 137ms 가 출력되었으나 현재는 23ms 정도로 대폭 감소한 것을 확인할 수 있었습니다.


image


Redis 에서도 categoryList 라는 키 이름으로 지정된 캐시 데이터가 잘 저장된 것을 확인할 수 있었습니다! 해당 내용은 커맨트창에서 keys * 를 입력하면 현재 캐시된 키 들의 리스트를 확인해볼 수 있습니다.



정리하며

캐시 히트율도 같이 계산을 해보면 더 의미가 있었을 것 같지만, 현재 서버 배포하기 전 단계라 LRU Simulation 은 확인할 수 없었습니다. 이 후 배포까지 완료를 한다면 실제 테스트를 해보며 유의미한 캐시 히트율과 그에 따른 entryTtl , Cache evict 부분을 튜닝 해보면 좋겠다는 생각이 듭니다:)

끝으로 캐싱에 대한 저의 긴 글 끝까지 읽어주셔서 감사합니다~!



Project Github URL

오구리이미지

FESTA 프로젝트 Github 보러가기 Click!




Referenced by







Categories:

Updated: