Spring

Spring - Redis를 사용해보자

Cold Bean 2023. 4. 15. 01:19
728x90

배경

진행 중인 프로젝트의 Security 작업을 담당하면서 JWT Refresh Token을 Redis에 저장하여 관리하기로 했다.

 

왜 Redis?

Redis는 메모리 기반의 Key-Value 저장소이다. 메모리에 데이터를 저장하기 때문에 훨씬 빠르게 데이터에 접근할 수 있다. 따라서, Redis를 캐시로 사용하면 웹 서버에서 반복적으로 사용되는 데이터를 메모리에 저장하여 매번 데이터베이스에서 읽어오는 비용을 아낄 수 있다.

 

가장 큰 이유는 Redis는 TTL(Time-To-Live) 기능을 제공하기 때문에 데이터의 만료 시간을 설정할 수 있기 때문이다. 이를 통해 서버에 저장할 Refresh Token의 만료 시간을 쉽게 설정할 수 있게 된다. 만료된 Refresh Token은 Redis에서 자동으로 삭제된다.

 

Refresh Token 말고도 반복적으로 사용되는 데이터는 Redis 캐싱으로 관리하기 위해서 애플리케이션에 적용할 필요가 있었다.

 

구현

Build.gradle

Spring Data Redis를 gralde에 추가해 준다.

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

 

RedisConfig

Redis와의 연결 정보를 설정하고, Redis 데이터를 저장하고 조회하는 데 사용되는 RedisTemplate 객체를 생성하는 역할을 하는 클래스

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

    // RedisProperties로 yaml에 저장한 host, post를 연결
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
    }

    // serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 설정
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        return redisTemplate;
    }
}
  • @EnableRedisRepositories : Redis를 사용하한 다고 명시해 주는 애너테이션
  • RedisProperties : Redis 서버와의 연결 정보를 저장하는 객체이다. redis의 host와 port를 YAML 파일에서 수정할 수 있고 redisProperties.getHost(), redisProperties.getPort() 메서드를 통해 가져올 수 있다.

 

spring:
		redis:
		    host: localhost
		    port: 6380
  • redisConnectionFactory() : LettuceConnectionFactory 객체를 생성하여 반환하는 메서드. 이 객체는 Redis Java 클라이언트 라이브러리인 Lettuce를 사용해서 Redis 서버와 연결해 준다.
  • redisTemplate() : RedisTemplate 객체를 생성하여 반환한다. RedisTemplate은 Redis 데이터를 저장하고 조회하는 기능을 하는 클래스이다.
  • setKeySerializer() , setValueSerializer() : Redis 데이터를 직렬화하는 방식을 설정할 수 있다. Redis CLI를 사용해 Redis 데이터를 직접 조회할 때, Redis 데이터를 문자열로 반화해야하기 때문에 설정한다.

 

RedisService

Redis에 저장, 조회, 삭제하는 메서드를 구현하는 클래스. RedisTemplate를 주입받아 Redis 데이터를 조작한다.

@Slf4j
@Component
@RequiredArgsConstructor
public class RedisService {
    private final RedisTemplate<String, Object> redisTemplate;

    public void setValues(String key, String data) {
        ValueOperations<String, Object> values = redisTemplate.opsForValue();
        values.set(key, data);
    }

    public void setValues(String key, String data, Duration duration) {
        ValueOperations<String, Object> values = redisTemplate.opsForValue();
        values.set(key, data, duration);
    }

    @Transactional(readOnly = true)
    public String getValues(String key) {
        ValueOperations<String, Object> values = redisTemplate.opsForValue();
        if (values.get(key) == null) {
            return "false";
        }
        return (String) values.get(key);
    }

    public void deleteValues(String key) {
        redisTemplate.delete(key);
    }

    public void expireValues(String key, int timeout) {
        redisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS);
    }

    public void setHashOps(String key, Map<String, String> data) {
        HashOperations<String, Object, Object> values = redisTemplate.opsForHash();
        values.putAll(key, data);
    }

    @Transactional(readOnly = true)
    public String getHashOps(String key, String hashKey) {
        HashOperations<String, Object, Object> values = redisTemplate.opsForHash();
        return Boolean.TRUE.equals(values.hasKey(key, hashKey)) ? (String) redisTemplate.opsForHash().get(key, hashKey) : "";
    }

    public void deleteHashOps(String key, String hashKey) {
        HashOperations<String, Object, Object> values = redisTemplate.opsForHash();
        values.delete(key, hashKey);
    }

    public boolean checkExistsValue(String value) {
        return !value.equals("false");
    }
}
  • setValues() : key와 data를 Redis에 저장한다. 만약 데이터에 만료 시간을 설정하고 싶다면 세 번째 파라미터로 Duration 객체를 전달한다.
  • getValues() : key 파라미터로 받아 key를 기반으로 데이터를 조회한다.
  • deleteValues() : key를 파라미터로 받아 key를 기반으로 데이터를 삭제한다.
  • checkExistsValue() : 조회하려는 데이터가 없으면 “false”를 반환한다.

 

테스트

Redis CRUD가 잘 작동하는지 테스트를 진행했다.

@Slf4j
@SpringBootTest
class RedisCrudTest {
    final String KEY = "key";
    final String VALUE = "value";
    final Duration DURATION = Duration.ofMillis(5000);
    @Autowired
    private RedisService redisService;

    @BeforeEach
    void shutDown() {
        redisService.setValues(KEY, VALUE, DURATION);
    }

    @AfterEach
    void tearDown() {
        redisService.deleteValues(KEY);
    }

    @Test
    @DisplayName("Redis에 데이터를 저장하면 정상적으로 조회된다.")
    void saveAndFindTest() throws Exception {
        // when
        String findValue = redisService.getValues(KEY);

        // then
        assertThat(VALUE).isEqualTo(findValue);
    }

    @Test
    @DisplayName("Redis에 저장된 데이터를 수정할 수 있다.")
    void updateTest() throws Exception {
        // given
        String updateValue = "updateValue";
        redisService.setValues(KEY, updateValue, DURATION);

        // when
        String findValue = redisService.getValues(KEY);

        // then
        assertThat(updateValue).isEqualTo(findValue);
        assertThat(VALUE).isNotEqualTo(findValue);
    }

    @Test
    @DisplayName("Redis에 저장된 데이터를 삭제할 수 있다.")
    void deleteTest() throws Exception {
        // when
        redisService.deleteValues(KEY);
        String findValue = redisService.getValues(KEY);

        // then
        assertThat(findValue).isEqualTo("false");
    }

    @Test
    @DisplayName("Redis에 저장된 데이터는 만료시간이 지나면 삭제된다.")
    void expiredTest() throws Exception {
        // when
        String findValue = redisService.getValues(KEY);
        await().pollDelay(Duration.ofMillis(6000)).untilAsserted(
                () -> {
                    String expiredValue = redisService.getValues(KEY);
                    assertThat(expiredValue).isNotEqualTo(findValue);
                    assertThat(expiredValue).isEqualTo("false");
                }
        );
    }
}

 

테스트 성공!

728x90