본문 바로가기

Spring Boot

Spring Boot에서 Redis를 활용해 데이터 캐싱하기(feat. Look Aside)

캐시는 자주 사용되는 데이터를 미리 복사해 저장하는 임시 저장소이다. 다음과 같은 상황에서 캐시를 사용할 수 있다.

 

  • 같은 데이터에 반복적으로 접근하는 상황
  • 동시에, 해당 데이터의 변경이 적은 상황

캐시를 구현하기 위해 Redis가 많이 사용된다. 그 이유는 다음과 같다.

Redis는 In memory 데이터베이스로, Key, Value 쌍을 저장하는 일종의 NoSQL DB이다. In memory 데이터베이스이므로, 하드 디스트에 데이터를 저장하는 일반적인 데이터베이스보다 속도가 월등히 빠르다.

 

물론, 단점도 있다. 접근 속도에 대한 Trade Off로 비싸다. 비싸다는 것은, 용량이 적다는 것을 나타낸다. 따라서, 많은 데이터를 Redis에 저장하기엔 한계가 있다.

 

캐싱을 잘 사용하면 특정 상황에서 애플리케이션의 응답 속도를 크게 향상시킬 수 있다.

 

해당 포스팅에서는 Redis 데이터베이스를 사용한 읽기 전용 캐싱을 구현할 것이다.
그 전에, 읽기 캐싱 전략을 설정해야 한다.
읽기 캐싱 전략에는 여러 가지가 있지만, 필자는 Look Aside 전략을 사용할 것이다.

 

캐싱 전략과 관련된 내용은 해당 글에 정리되어 있다.

https://dev-allday.tistory.com/82

 

 

 

1. Redis 의존성 추가


Redis를 사용하기 위해 build.gradle에 의존성을 추가한다.

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

 

 

 

 

2. RedisConfig 작성


@Configuration
public class RedisConfig {
    @Value("${spring.data.redis.host}")
    private String redisHost;
    @Value("${spring.data.redis.port}")
    private int redisPort;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public <K, V> RedisTemplate<K, V> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<K, V> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new GenericJackson2JsonRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

 

Redis의 설정을 해 주는 Config 클래스를 작성한다. 

자바의 Redis Client 라이브러리는 JedisLettuce가 있는데, Jedis와 달리 Lettuce 는 스레드 세이프한 라이브러리로, 대규모 트래픽에서 성능이 더 좋다고 한다. Lettuce를 사용하겠다.

LettuceConnectionFactoryRedis와 연결을 만든다.

 

다음은 RedisTemplate인데, Redis에 데이터를 저장하기 위한 구현체는 RedisRepository, RedisTemplate가 있다.

필자는 RedisTemplate을 사용할 것이다.

 

RedisTemplateKeyValueRedis에 간단하게 데이터를 넣을 수 있는 수단을 제공한다.

그 중, 재사용성을 위해 Generic으로 이루어진 RedisTemplate<K, V>을 사용할 것이다.

KeyValue에 들어 올 객체를 Redis DB에 넣거나 조회하기 위해 직렬화, 역직렬화를 해야 하는데, 이를 위해 GenericJackson2JsonRedisSerializer를 사용했다.

 

GenericJackson2JsonRedisSerializer는 Spring Data Redis에서 제공하는 직렬화 도구로, Jackson 라이브러리를 사용하여 객체를 JSON 형식으로 직렬화하고 역직렬화한다. 이를 통해 Redis에 저장되는 데이터가 JSON 형식으로 변환되며, 이를 읽을 때 다시 객체로 변환할 수 있다.

 

KeySerializer, ValueSerializerGenericJackson2JsonRedisSerializer를 넣어 준다.

 

 

 

 

 

 

3. RedisUtil 작성


이제 RedisTemplate을 사용해 Redis와 실제로 상호작용할 클래스를 만들 것이다. 위에서 Bean으로 등록한 RedisTemplate<K, V>를 주입받아 Redis와 실제로 상호작용하는 RedisUtil 클래스를 만들어 보자.

 

@Service
public class RedisUtil<K, V> {
    private final RedisTemplate<K, V> redisTemplate;
    private final long offset;

    public RedisUtil(RedisTemplate<K, V> redisTemplate, @Value("${spring.data.redis.offset}") long offset) {
        this.redisTemplate = redisTemplate;
        this.offset = offset;
    }

    public void insert(K key, V value) {
        try {
            this.redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(offset));
        } catch (Exception e) {
            throw new RedisException(REDIS_ERROR, e.getMessage());
        }
    }

    public Optional<V> select(K key) {
        try {
            V value = this.redisTemplate.opsForValue().get(key);
            return Optional.ofNullable(value);
        } catch (Exception e) {
            throw new RedisException(REDIS_ERROR, e.getMessage());
        }
    }
}

 

RedisTemplate<K, V>를 주입받고, application.properties에서 offset을 받아 온다. offset은 만료 시간이다. 필자는 36000(10시간)으로 임시 설정했다.

 

insert, select에서는 opsForValue()를 사용했다. 

Redis는 여러 자료 구조를 가지고 있는데, 여러 종류의 자료구조를 다루기 위해 Spring Data Redis는 opsForValue, opsForSet, opsForZSet 등의 메서드를 제공한다. 해당 메서드를 사용하면 각 자료구조에 대해서 쉽게 직렬화, 역직렬화할 수 있다.

메서드 설명
opsForValue String의 직렬화/역직렬화 Interface
opsForList List의 직렬화/역직렬화 Interface
opsForSet Set의 직렬화/역직렬화 Interface
opsForZSet ZSet의 직렬화/역직렬화 Interface
opsForHash Hash의 직렬화/역직렬화 Interface

 

우리는 아까 KeyValueSerializerGenericJackson2JsonRedisSerializer을 사용했다. GenericJackson2JsonRedisSerializer은 해당 객체를(Key나 Value로 들어 온 객체를) JSON 형태의

"문자열"로 바꿔 직렬화, 역직렬화를 수행한다. 그러므로, 필자는 opsForValue()를 사용했다.

 

opsForValue()의 set()과 get()을 통해 Redis에 데이터를 넣거나 조회할 수 있다.

 

필자는 Redis에 넣을 모든 데이터의 TTL(만료 시간)이 동일해도 상관없었기에 위 방법으로 설계했지만, 상황에 따라 설계 방법은 달라질 수 있다.

 

 

 

 

 

 

 

4. RedisUtil 적용 with Look Aside


private Car test() {
    Car response = null;// 우선 null로 초기화
    try {
        Optional<Car> car = redisUtil.select(id);
        if (car.isPresent()) {
            return car.get();// redis에서 가져온 값이 있으면 반환
        }
        response = carRepository.findById(id);// 없으면 DB에서 가져옴
        redisUtil.insert(id, response);// 가져온 값을 redis에 저장
    } catch (RedisException e) { // RedisException 발생 시 DB에서 가져옴
        return carRepository.findById(id);// DB에서 가져옴
    }
    return response;// DB에서 가져온 값 반환
}

 

 

이런 식으로 RedisTemplate<K, V>와 Look Aside 전략을 사용해 캐싱을 구현할 수 있다.