스프링을 연동한 프로젝트를 진행 중, DB에 부하를 적게 주는 방법에 대해 고민하다 기존의 spring-boot-starter에 내장되어있는 Redis의 캐싱에 대해 알게됐습니다.

spring-boot-starter에 내장되어있는 Redis 라이브러리를 사용하면 Redis의 사용법 중 Key-Value로 사용할 수 있는 RedisTemplate, RedisRepository등이 있습니다.

이번 포스팅에서는 입찰 API에서 RedisTemplate를 사용한 이유, 그리고 사용한 예시들에 대해 알아보겠습니다.

 

Redis

  • Key,Value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈소스 기반의 DBMS
  • 단일 쓰레드(SingleThread) 형식으로 구성되어 있다.
    • 멀티 스레드보다 적은 메모리를 사용하며 스레드간의 자원 공유와 동기화가 없어 자원관리에 용이하다.
    • 하나의 작업이 시작되면 블로킹되어  다른 작업을 수행할 수 없다. 남용할 시 어플리케이션 응답시간에 영향을 줄 수 있다.
  • 데이터에서 디스크에 쓰는 구조가 아닌 메모리에서 데이터를 처리하기때문에 MySQL의 DB보다 속도가 빠르다.
  • 싱글 쓰레드의 특성상, 한번에 하나의 명령만 처리가능하기때문에 처리하기 오래걸리는 요청,명령은 사용하기에 부적합하다.
  • String, Lists, Sets, Sorted Sets, Hashes등의 자료구조를 지원한다.
  • 메모리 기반의 데이터 저장소로 디스크에 지속적으로 저장하지 않아 시스템 재시작 또는 서버 장애 발생시에 데이터가 손실될 수 있다. 
    • RDB 스냅샷 혹은 AOF 로그 등을 통해 복구가 가능하다. 그 외에도 다양한 보완 대책을 마련하여 영속성을 보장시킬 수 있다. 

  RDB 스냅샷 : 주기적으로 데이터의 스냅샷을 생성하여 데이터베이스의 현재 상태를 저장하는 방식

  AOF(Append-Only File) : 명령의 로그를 유지함으로써 재시작 시에 로그를 replay하여 데이터를 복구하는 방식

 

RedisTemplate VS RedisRepository

 

RedisTemplate

  • Redis의 모든 데이터 유형을 Object형으로  접근할 수 있다.
  • 데이터 형식을 어떻게 직렬화(Serializer), 역직렬화(Deserializer)할지 설정할 수있다.
    • RedisRepository : JdkSerializationRedisSerializer를 사용하여 객체를 Byte 배열로 직렬화, 역직렬화를 한다.
    • RedisTemplate : Jackson2JsonRedisSerializer를 사용한 JSON 직렬화 설정, Key와 Value에 대한 직렬화와 역직렬화 설정을 String과 같은 다양한 설정이 가능하다.
  • RedisRepository보다 더 세밀한 작업이 가능하다.
  • ReisTemplate를 사용하기 위해서는 Redis와 상호작용하는 코드를 직접 작성해야하므로 초기 개발시의 복잡성이 높다.

RedisRepository

  • 어노테이션을 사용하여 간단하게 Redis 데이터에 접근할 수 있다.
  • 인터페이스 기반 프로그래밍을 통해 개발자가 직접 Redis의 구성을 하지 않아도 된다.
  • Redis의 모든 데이터 유형을 지원하지 않는다.
  • 처리하는 속도가 RedisTemplate보다 늦다.

RedisTemplate와 RedisRepository에 대해 간단하게 알아보았으며, 입찰 API에 Redis를 사용하기위해서는

RedisRepository의 방법을 사용하려 하였으나 실제로 사용해보았을 때 특정 데이터값들만 캐싱할 수 없다는 단점을 가지고 있어 RedisTemplate를 사용하게 되었습니다.

 

실제 코드에서 RedisTemplate를 설정한 방법 및 사용 예시에 대해 알아보겠습니다.

 

준비

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

build.gradle에 의존성을 주입합니다.

 

spring.redis.host=localhost
spring.redis.port=6379

application.properity에 호스트 및 포트 설정을 추가합니다.

 

RedisCoinfig.class

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

    @Bean
    public RedisTemplate<?, ?> redisTemplate() {
        RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        GenericJackson2JsonRedisSerializer serializer =
                new GenericJackson2JsonRedisSerializer();

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(serializer);

        return redisTemplate;
    }

    @Bean
    public RedisCacheManager redisCacheManager() {

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
                .defaultCacheConfig()
                .disableCachingNullValues()
                .serializeKeysWith(
                        RedisSerializationContext.SerializationPair
                                .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair
                                .fromSerializer(new GenericJackson2JsonRedisSerializer())
                )
                .entryTtl(Duration.ofDays(1L));

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

RedisConfig 클래스를 생성 후 연결하기위한 Factory, Template 및 CacheManager를 설정하는 과정입니다.

 

RedisConnectionFactory : Redis에 커넥션시에 객체를 생성하여 관리하는 인터페이스

RedisCacheManager : 스프링에서 추상화 인터페이스 CacaheManager를 레디스 방식으로 구현한 클래스

RedisTemplate : Template를 사용하여 Redis를 사용시 직렬화, 역직렬화를 어떻게 할 것인지에 대한 설정

 

 

BidItemDAO.class

    public Bid readBidWithCache(int bidId) {
        String key = generateBidKey(bidId);
        String cacheKey = readCacheKey(bidId);

        if (cacheKey != null) {
            Bid cacheBid = redisTemplate.opsForValue().get(cacheKey);
            return cacheBid;
        } else {
            Bid bid = bidMapper.readBidWithItemID(bidId);
            if (bid != null) {
                redisTemplate.opsForValue().set(key, bid);
                redisTemplate.expire(key, 1, TimeUnit.DAYS);
            }
            return bid;
        }
    }

데이터 조회/삽입시의 예시코드입니다.

Redis에 선택한 입찰상품ID가 없을시에는 Mapper에서 Mybatis를 사용한 데이터를 가져오며, 데이터가 캐싱되어있을경우 메모리에 저장되어있는 캐시 값을 가져와서 사용합니다.

 

 

    private String readCacheKey(int bidId) {
        String matchKey = bidId + bidKey;
        ScanOptions scanOptions = ScanOptions.scanOptions()
                .match("*:" + matchKey)
                .count(50)
                .build();

        Cursor<String> cursor = redisTemplate.scan(scanOptions);

        if (cursor.hasNext()) {
            String key = cursor.next();
            cursor.close();
            return key;
        } else {
            cursor.close();
            return null;
        }
    }

Redis를 사용하여 캐싱된 데이터들을 조회할 때 사용하는 메서드입니다.

기존의 데이터값을 조회할때는 redisTemplate.keys를 사용하여 모든 key값들을 탐색하는 시간복잡도 O(n)의 형태를 가지고 있었습니다.

저는 싱글쓰레드 구조의 Redis의 캐시 데이터가 많아질수록 keys로 탐색시 블로킹 시간이 더 오래 걸리는 방법을 해결하기위해 ScanOptions를 사용하게 되었습니다.  

 

※블로킹 : 대상의 작업이 끝날 때 까지 제어권을 대상이 가지고 있으며, 대상의 작업 완료 여부에 따라 새로운 작업을 수행한다.

 

Scan vs KeyDB의 차이점 및 ScanOption의 장점

 

예시) Redis Key 10000개일시 탐색 구조

Keys : size의 값만큼 1부터 10000까지 조회를 완료할때까지 다른 행동은 블로킹 된다.

ScanOptions : Count 100일시, 1~100조회, 블로킹 해제 후 다른 요청에 대한 응답, 101~200 조회 ... 

 

예시의 구조처럼 ScanOptions는 싱글쓰레드의 단점인 블로킹이 지속되는동안 다른 요청에대한 응답을 못하는것을 해결할 수 있었으며 이러한 이유로 ScanOptions를 사용하게 되었습니다.

 

결과 및 느낀점

RedisInSight를 사용하여 캐싱한 데이터를 조회할 수 있습니다.

입찰 API가 실행시에 Key-Value형태의 캐싱된 데이터를 확인할 수있었습니다.

입찰 상품에대한 ID가 존재하지않을시에는 Mybatis를 사용하여 호출하지만, 재호출시에는 Redis를 사용하여 메모리에 캐싱되어있는 값을 조회하기때문에 DB에 대한 부하를 줄일 수 있었으며, 메모리에 저장하는 캐싱의 특징으로 인하여 더빠른 응답속도를 가질 수 있게되었습니다.

 

 

기존의 Redis의 사용목적은 상품조회API에서 사용하기위해 Redis를 도입하였으나 데이터 정합성이 떨어지며 성능테스트 결과 시간 경과에 따라 데이터 캐싱 적용 전보다 0.1초에서 1.5초까지 응답 속도가 저하되는것을 확인했습니다. 이러한 이유로 조회API에서는 사용하지 않으며, 왜 사용하지 않는지에 대해서는 성능테스트에 관련된 포스트 글에서 비교하며 작성하도록 하겠습니다.

 

출처

https://docs.keydb.dev/blog/2020/08/10/blog-post/

https://zmfpdl64.tistory.com/3

+ Recent posts