본문 바로가기
Spring/SpringBoot

[SpringBoot] Redis 사용하기 (3) - Redis Template 사용하기

by J4J 2024. 5. 13.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 redis 사용하기 세 번째인 redis template 사용하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

이전 글

 

[SpringBoot] Redis 사용하기 (1) - Redis란 무엇인가?

[SpringBoot] Redis 사용하기 (2) - Redis Repository 사용하기

 

 

반응형

 

 

Redis Template이란 ?

 

redis template은 redis와 상호 작용할 수 있는 설정 및 기능들의 인터페이스를 제공해 주는 클래스입니다.

 

redis template이 무엇인지에 대해 알아보기 위해 먼저 이전 글에서 작성했던 redis repository 비교해 보겠습니다.

 

 

 

먼저 redis와 통신하기 위한 사용성 및 관리 측면입니다.

 

redis repository 같은 경우는 repository에서 필요로 하는 형태로 메서드를 구성한다면 redis와 통신할 수 있었습니다.

 

하지만 redis template 같은 경우는 단순 CRUD 처리를 제외한 나머지 설정 및 관리들을 모두 개발자가 직접 수행해야 합니다.

 

 

 

다음은 세부 설정 제어 방식입니다.

 

위에서 얘기한 것처럼 redis repository를 사용하면 repository의 구조에 맞게 모든 것을 설정해야 되기 때문에 개발자가 원하는 방향에 맞게 자유로운 설정이 불가합니다.

 

하지만 redis template은 redis repository처럼 정해져 있는 것에만 맞추지 않고 개발되는 서비스의 프로젝트 상황에 따라 개발자에 맞춘 여러 설정들을 자유롭게 시도해 볼 수 있습니다.

 

 

 

 

결론적으로 redis repository는 repository에서 자동으로 설정해 주는 다양한 기능들을 활용하는 방식입니다.

 

그러나 redis template은 개발자의 상황과 환경을 고려하여 정해지지 않은 여러 가지 다양한 시도들을 프로젝트 별로 수행해볼 수 있습니다.

 

 

 

redis repository와 redis template 중 어떤 것을 사용해야 되는지는 정답이 없다고 생각합니다.

 

정해진 설정 방식을 통해 간단한 기능 구현을 할 때는 redis repository가 더 이점을 가져다줄 수 있지만 상세 비즈니스 로직 처리에 대한 부분을 바라보면 redis template이 더 강점을 보입니다.

 

최근에 redis를 사용하시는 분들은 redis repository보다는 redis template을 더 많이 선호하는 것으로 보이는데 프로젝트 상황 별 더 적합하다고 판단되는 것을 최종적으로 선택해서 사용하면 될 것 같습니다.

 

 

 

 

Redis Template 사용 환경 설정

 

이번에는 redis template을 활용할 수 있도록 환경 설정을 해보겠습니다.

 

redis repository 때와 동일하게 사용자 정보를 redis에 저장하는 코드를 예시로 작성하면 다음과 같습니다.

 

 

 

[ 1. dependency 추가 ]

 

dependencies {
	// redis
	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

 

 

 

[ 2. redis properties 추가 ]

 

spring:
  data:
    redis:
      host: localhost # redis host 입력
      port: 6379 # redis port 입력

 

 

 

[ 3. redis config 클래스 추가 ]

 

package com.jforj.redistemplate.config;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@RequiredArgsConstructor
public class RedisConfig {
    private final RedisProperties redisProperties;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(
                new RedisStandaloneConfiguration(
                        redisProperties.getHost(),
                        redisProperties.getPort()
                )
        );
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate() {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();

        // redis template에 connection factory 연결
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        // key에 대한 직렬화 방법 등록
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // value에 대한 직렬화 방법 등록
        redisTemplate.setValueSerializer(new StringRedisSerializer());

        // hash key에 대한 직렬화 방법 등록
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // hash value에 대한 직렬화 방법 등록
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());

        return redisTemplate;
    }
}

 

 

 

 

[ 4. user entity 추가 ]

 

package com.jforj.redistemplate.entity;

import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

@Builder
@Getter
@ToString
public class User {
    private String id;
    private String name;
}

 

 

 

[ 5. user repository 추가 ]

 

package com.jforj.redistemplate.repository;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jforj.redistemplate.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Repository
@RequiredArgsConstructor
public class UserRepository {
    private final RedisTemplate<String, String> redisTemplate;
    private final ObjectMapper objectMapper;

    public void create(String id, String name) {
        User user = User.builder()
                .id(id)
                .name(name)
                .build();

        setRedis(id, user);
    }

    public void selectAll() {
        List<String> keys = new ArrayList<>(redisTemplate.keys("*"));
        List<User> users = getRedisValue(keys, User.class);

        System.out.println(users);
    }

    /**
     * value 값을 직렬화하여 redis에 저장
     */
    private void setRedis(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value));
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 직렬화되어 저장된 value 값을 redis에서 조회 후 class type에 맞게 변환
     */
    private <T> T getRedisValue(String key, Class<T> valueClass) {
        String value = redisTemplate.opsForValue().get(key);
        if (!StringUtils.hasText(value)) {
            return null;
        }

        try {
            return objectMapper.readValue(value, valueClass);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 직렬화되어 저장된 value 값을 redis에서 조회 후 class type에 맞게 변환
     */
    private <T> List<T> getRedisValue(List<String> keys, Class<T> valueClass) {
        List<String> values = redisTemplate.opsForValue().multiGet(keys);
        if (values.size() < 1) {
            return new ArrayList<>();
        }

        return values
                .stream()
                .map(value -> {
                    try {
                        return objectMapper.readValue(value, valueClass);
                    } catch (JsonProcessingException e) {
                        throw new RuntimeException(e);
                    }
                })
                .collect(Collectors.toList());
    }
}

 

 

 

repository 내부 메서드를 확인해 보면 setRedis와 getRedisValue가 존재합니다.

 

해당 메서드가 존재하는 이유는 redisConfig 클래스의 redisTemplate serializer 설정과 관련이 있습니다.

 

아래에서 serializer와 관련된 부분을 더 자세하게 다뤄볼 것이긴 한데 먼저 위와 같이 설정이 되어 있는 이유에 대해서 간단히 얘기해 보겠습니다.

 

 

 

redisConfig 클래스를 확인해 보면 value 값을 저장하기 위해 stringRedisSerializer를 사용하고 있습니다.

 

그러면 value 값을 redis에 저장할 때 string으로 전달해줘야 하기 때문에 저장해야 하는 모든 데이터를 직렬화하여 string으로 변환하기 위함입니다.

 

반대로 redis에서 조회할 때는 string으로 저장되어 있는 value 값을 가져오기 때문에 우리가 원하는 형태의 객체 구조로 역직렬화해줘야 합니다.

 

그래서 이런 역할을 처리하는 공통 기능들을 정의하기 위해 setRedis와 getRedisValue 메서드들이 사용되고 있습니다.

 

 

 

 

위와 같이 작성한 코드들이 올바르게 동작하는지 테스트를 해보겠습니다.

 

repository 테스트 코드를 작성한 뒤 각각 실행해 보면 다음과 같은 결과를 확인할 수 있습니다.

 

package com.jforj.redistemplate.repository;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void create() {
        userRepository.create("first id", "first name");
        userRepository.create("second id", "second name");
    }

    @Test
    void selectAll() {
        userRepository.selectAll();
    }
}

 

create test

 

redis key, value 정보

 

selectAll test

 

 

 

 

Serializer 설정

 

serializer 설정은 redis template을 이용하여 key, value 값을 어떤 방식으로 직렬화 처리할지를 결정합니다.

 

여러 가지 설정 값이 있지만 대표적인 것들만 정리해 보겠습니다.

 

 

 

[ 1. JdkSerializationRedisSerializer ]

 

먼저 JdkSerializationRedisSerializer입니다.

 

해당 serializer는 redis config에 아무것도 설정하지 않으면 적용되어 있는 default 설정 값입니다.

 

java에서 제공해 주는 직렬화 방식을 사용하는 설정 값으로 redis에 저장하려는 객체에 Serializable 인터페이스를 상속시켜주면 됩니다.

 

 

 

JdkSerializationRedisSerializer를 사용하면 객체를 바이트 스트림 형태로 변환하여 redis에 저장되며 저장된 key, value를 확인하면 바이트 스트림 형태로 변경된 결과물과 package 구조 및 객체 명 등을 확인할 수 있습니다.

 

이런 정보들은 결국 역직렬화를 올바르게 수행할 수 없는 결과를 만들 수 있기 때문에 일반적으로 채택되지 않는 serializer 설정입니다.

 

 

 

해당 serializer 설정 방법을 확인하기 위해 사용 환경 설정 코드에서 일부 소스 코드를 변경한 뒤 테스트를 수행하면 다음과 같은 결과를 확인할 수 있습니다.

 

// redis config
@Configuration
@RequiredArgsConstructor
public class RedisConfig {

    ...
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        // redis template에 connection factory 연결
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        return redisTemplate;
    }
}


// user entity
@Builder
@Getter
@ToString
public class User implements Serializable {

    ...
    
}


// user repository
@Repository
@RequiredArgsConstructor
public class UserRepository {
    private final RedisTemplate<String, Object> redisTemplate;

    ...
    
    public void create(String id, String name) {
        User user = User.builder()
                .id(id)
                .name(name)
                .build();

        redisTemplate.opsForValue().set(id, user);
    }
    
    ...
    
}

 

JdkSerializationRedisSerializer 설정 후 create test

 

JdkSerializationRedisSerializer 설정 후 redis key, value 정보

 

 

 

 

[ 2. GenericJackson2JsonRedisSerializer ]

 

GenericJackson2JsonRedisSerializer은 redis에 저장하려는 객체들을 json 기반의 문자열로 직렬화하는 설정입니다.

 

해당 serializer를 사용하면 추가적인 행위 없이 객체를 자동으로 직렬화를 할 수 있게 도와줍니다.

 

하지만 이 방법의 문제점은 redis에 저장된 value 값을 확인해 보면 package 구조를 포함한 클래스 명이 함께 적재되어 있다는 것입니다.

 

그래서 역직렬화를 할 때 구조에 맞는 클래스 명이 아니라면 문제를 발생시킬 수 있습니다.

 

해당 클래스가 의존되고 있는 다양한 서비스들이 존재할 경우 모든 서비스에서 이를 지켜줘야 하기 때문에 관리하기 어려운 결과를 만들 수 있습니다.

 

 

 

이 또한 serializer 설정 방법을 확인하기 위해 사용 환경 설정 코드에서 일부 소스 코드를 변경한 뒤 테스트를 수행하면 다음과 같은 결과를 확인할 수 있습니다.

 

// redis config
@Configuration
@RequiredArgsConstructor
public class RedisConfig {

    ...
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        // redis template에 connection factory 연결
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        // key에 대한 직렬화 방법 등록
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // value에 대한 직렬화 방법 등록
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        // hash key에 대한 직렬화 방법 등록
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // hash value에 대한 직렬화 방법 등록
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        return redisTemplate;
    }
}


// user entity

...

@NoArgsConstructor // 역직렬화를 위해 default 생성자 필요
public class User {

    ...
    
}


// user repository
@Repository
@RequiredArgsConstructor
public class UserRepository {
    private final RedisTemplate<String, Object> redisTemplate;

    ...
    
    public void create(String id, String name) {
        User user = User.builder()
                .id(id)
                .name(name)
                .build();

        redisTemplate.opsForValue().set(id, user);
    }
    
    public void selectAll() {
        List<String> keys = new ArrayList<>(redisTemplate.keys("*"));
        List<Object> users = redisTemplate.opsForValue().multiGet(keys);

        System.out.println(users);
    }
    
    ...
    
}

 

GenericJackson2JsonRedisSerializer 설정 후 create test

 

GenericJackson2JsonRedisSerializer 설정 후 redis key, value 정보

 

GenericJackson2JsonRedisSerializer 설정 후 selectAll test

 

 

 

여기서 위에서 얘기한 역직렬화에 대한 문제점을 확인하기 위해 user entity의 클래스 정보를 변경해 보면 어떻게 되는지 확인해 보겠습니다.

 

user entity의 이름을 User에서 MyUser라고 변경한 뒤 selectAll test를 수행하면 다음과 같은 결과를 확인할 수 있습니다.

 

GenericJackson2JsonRedisSerializer 설정 및 entity 변경 후 selectAll test

 

 

 

 

[ 3. Jackson2JsonRedisSerializer ]

 

세 번째는 Jackson2JsonRedisSerializer 입니다.

 

해당 serializer와 GenericJackson2JsonRedisSerializer의 차이점은 redis에 저장되는 value 값을 확인했을 때 클래스 정보가 없다는 것입니다.

 

그래서 클래스의 구조 자체가 다양한 서비스에서 의존되지 않기에 자유롭게 사용할 수 있습니다.

 

하지만 serializer 설정을 할 때 redis에 저장되어 직렬화가 발생될 수 있는 모든 객체 정보를 등록해줘야 하는 단점이 존재합니다.

 

1~2개 정도 작은 단위로 사용되는 것에는 지장이 없겠지만 프로젝트의 규모가 조금만 커도 몇 십 개의 클래스를 등록해줘야 하기에 저장되어야 하는 정보가 많아질수록 관리되어야 하는 클래스가 동일하게 많아지는 불편함이 있습니다.

 

 

 

설정 방법을 확인하기 위해 사용 환경 설정 코드에서 일부 코드를 변경하여 테스트해 보면 다음과 같은 결과를 확인할 수 있습니다.

 

// redis config
@Configuration
@RequiredArgsConstructor
public class RedisConfig {

    ...
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        // redis template에 connection factory 연결
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        // key에 대한 직렬화 방법 등록
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // value에 대한 직렬화 방법 등록
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(User.class));
        // redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(~~.class));

        // hash key에 대한 직렬화 방법 등록
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // hash value에 대한 직렬화 방법 등록
        redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(User.class));
        // redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(~~.class));

        return redisTemplate;
    }
}


// user entity

...

@NoArgsConstructor // 역직렬화를 위해 default 생성자 필요
public class User {

    ...
    
}


// user repository
@Repository
@RequiredArgsConstructor
public class UserRepository {
    private final RedisTemplate<String, Object> redisTemplate;

    ...
    
    public void create(String id, String name) {
        User user = User.builder()
                .id(id)
                .name(name)
                .build();

        redisTemplate.opsForValue().set(id, user);
    }
    
    public void selectAll() {
        List<String> keys = new ArrayList<>(redisTemplate.keys("*"));
        List<Object> users = redisTemplate.opsForValue().multiGet(keys);

        System.out.println(users);
    }
    
    ...
    
}

 

Jackson2JsonRedisSerializer 설정 후 create test

 

Jackson2JsonRedisSerializer 설정 후 redis key, value 정보

 

Jackson2JsonRedisSerializer 설정 후 selectAll test

 

 

 

 

[ 4. StringRedisSerializer ]

 

마지막으로 소개드릴 serializer는 사용 환경 설정 코드에서 적용되어 있는 StringRedisSerializer입니다.

 

해당 serializer는 위의 serializer 들과 다르게 자동으로 json 기반의 문자열로 변환하는 기능은 존재하지 않습니다.

 

그래서 redis에 데이터를 적재할 때부터 문자열의 형태로 변환이 되어 있어야 합니다.

 

하지만 이런 불편함을 감소하면 다른 serializer들이 가지고 있는 문제점들을 모든 상황에서 걱정하지 않아도 됩니다.

 

그래서 개인적으로는 StringRedisSerializer를 사용하는 것을 추천드리고 대신에 사용하실 것이라면 제가 작성했던 setRedis, getRedisValue 등과 같이 자동 변환해 주는 공통 메서드를 정의하여 사용하는 것을 권장드립니다.

 

 

 

 

Hash Serializer

 

위의 serializer 설정을 위해 redis config 클래스를 확인해 보면 setHash~~Serializer가 존재하는 것을 볼 수 있습니다.

 

해당 설정 정보들에 대해 설명하면 다음과 같습니다.

 

  • setKeySerializer → redis에 저장되는 key 정보에 대한 serializer 설정 (위 테스트 모든 경우가 여기에 해당)
  • setValueSerializer → redis에 저장되는 value 정보에 대한 serializer 설정 (위 테스트 모든 경우가 여기에 해당)
  • setHashKeySerializer → redis의 key에 매핑되는 hashing이 존재할 때 hashing의 key 정보에 대한 serializer 설정
  • setHashValueSerializer → redis의 key에 매핑되는 hashing이 존재할 때 hashing의 value 정보에 대한 serializer 설정

 

 

 

설명에 적어둔 것처럼 일반적인 경우는 모두 setKeySerializer와 setValueSerializer에 해당됩니다.

 

그리고 setHashKeySerializer와 setHashValueSerializer가 영향받는 상황은 redis에 데이터를 저장할 때 "opsForHash" 메서드를 사용할 때입니다.

 

위의 예제 코드들은 모두 데이터를 저장할 때 "opsForValue"를 활용하고 있고 이 외에도 관리하고자 하는 방식에 따라 List, Set, Hash 등을 선택할 수 있습니다.

 

여기서 Hash를 선택하게 되면 다른 메서드들과 다르게 hash key에 대한 정보를 추가적으로 넣어줘야 합니다.

 

이런 상황이 발생했을 때 영향 받는 serializer의 설정이 바로 setHashKeySerializer와 setHashValueSerializer입니다.

 

redisTemplate.opsForHash().put(key, hashKey, value);
redisTemplate.opsForHash().get(key, hashKey);

 

 

 

 

유효 시간 설정

 

redis template에서는 key 정보를 기준으로 TTL 설정을 통해 유효 시간을 제공해주고 있습니다.

 

redis template 또한 redis repository와 마찬가지로 TTL 설정을 해주지 않으면 유효 시간은 따로 존재하지 않습니다.

 

상황에 따라 유효 시간이 지나면 데이터가 파기되는 것을 원할 수 있기에 필요한 경우 다음과 같이 설정을 해볼 수 있습니다.

 

// user repository
@Repository
@RequiredArgsConstructor
public class UserRepository {
    private final RedisTemplate<String, String> redisTemplate;
    private final ObjectMapper objectMapper;

    public void create(String id, String name, Long expiration) {
        User user = User.builder()
                .id(id)
                .name(name)
                .build();

        setRedis(id, user);
        redisTemplate.expire(id, Duration.ofSeconds(expiration));
    }
    
    ...
    
}


// user repository test
@SpringBootTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void create() {
        userRepository.create("first id", "first name", 60L); // 60초 후 파기
        userRepository.create("second id", "second name", 30L); // 30초 후 파기
    }
    
    ...
    
}

 

 

 

 

유효 시간 설정 후 create test

 

만료 시간 전 redis key 정보

 

만료 시간 전 selectAll test

 

30초 후 redis key 정보

 

30초 후 selectAll test

 

60초 후 redis key 정보

 

60초 후 selectAll test

 

 

 

 

 

 

 

 

이상으로 redis 사용하기 세 번째인 redis template 사용하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글