Redis로 RT 마이그레이션 적용기 및 유닛테스트
우선 리프레시 토큰이롼?
Access Token의 유효기간을 짧게하여 보안도 높이고, 편의성도 챙기는 방법이다.
로그인을 완료하면, 유효기간이 짧은 Access Token과 유효기간이 긴 Refresh Token을 발급해준다.
Access Token은 기존에 사용하던 JWT 토큰이라고 생각하면 되고, Refresh Token은 Access Token이 만료되었을 때, 새로 발급해주는 토큰이라고 생각하면 된다.
기존 방식
기존의 점주사이드와 유저사이드의 refreshtoken은 유저쪽에 한개의 컬럼을 만들어 조회되는 방식이였다.
문제는 리프레시 토큰 개념을 활용하려면, 불가피하게 서버측에서 토큰 정보를 저장할 수 있는 곳이 필요했다…
당시에는 RT의 보관을 MVP 개발 기간이 짧아 그냥 Mysql에 저장했었다.
하지만, MVP 제작 후, 유저의 사용으로 인해 점차적으로 RT에 대한 조회가 많아지고 조회시 쿼리문이 실행되면서, 중요도에 비해 생각보다 조회가 많이 일어난다는 것을 캐치하였다.
Mysql에 저장하기에는, 중요한 데이터가 아니라고 판단했고, 조회시 쿼리문을 실행시켜야하고, 주기적으로 데이터가 무효해지는 하는 점을 감안하여 레디스로 마이그레이션하기로 했다
Redis 도입기
레디스는 key-value 쌍으로 데이터를 관리할 수 있는 데이터 스토리지이다. 모든 데이터를 메모리에(메인 메모리인 RAM) 저장하고 조회하는 in-memory 데이터 베이스 이다.
RAM에 저장하는 만큼 빠르게 조회가 가능하며, 다른 DB들보다 빠르고 가볍다는 장점이 있다.
- SpringBoot redis 의존성
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
build.gradle에 추가
- application.properties
#redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
- redis > config > RedisConfig.java
package com.readyvery.readyverydemo.redis.config;
import org.springframework.beans.factory.annotation.Value;
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.lettuce.LettuceConnectionFactory;
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
}
- redis > dao > RefreshToken.java
package com.readyvery.readyverydemo.redis.dao;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@AllArgsConstructor
@Getter
@Builder
@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 30)
public class RefreshToken {
@Id
private String id;
@Indexed
private String refreshToken;
public void update(String refreshToken) {
this.refreshToken = refreshToken;
}
}
- redis > repository > RefreshTokenRepository.java
package com.readyvery.readyverydemo.redis.repository;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.readyvery.readyverydemo.redis.dao.RefreshToken;
@Repository
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
Optional<RefreshToken> findByRefreshToken(String refreshToken);
}
- RefreshToken 저장 및 조회 통합테스트
package com.readyvery.readyverydemo.redis;
import static org.assertj.core.api.AssertionsForClassTypes.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.readyvery.readyverydemo.domain.repository.CeoRepository;
import com.readyvery.readyverydemo.redis.dao.RefreshToken;
import com.readyvery.readyverydemo.redis.repository.RefreshTokenRepository;
@SpringBootTest
public class RefreshTokenRepositoryTest {
@Autowired
private RefreshTokenRepository refreshTokenRepository;
@Autowired
private CeoRepository ceoRepository;
private final String testId = "1223v@naver.com";
private final String testRefreshToken = "testRefreshToken1";
@BeforeEach
public void setup() {
RefreshToken token = new RefreshToken(testId, testRefreshToken);
refreshTokenRepository.save(token);
}
@AfterEach
public void cleanup() {
refreshTokenRepository.deleteById(testId);
}
@Test
@DisplayName("RefreshToken 저장 및 조회 테스트")
public void testSaveAndFindById() {
// ID로 조회
refreshTokenRepository.findByRefreshToken(testRefreshToken)
.map(RefreshToken::getId) // RefreshToken 객체에서 ID를 추출합니다.
.flatMap(ceoRepository::findByEmail) // 추출된 ID를 이용하여 CEO를 조회합니다.
.ifPresent(user -> {
String userEmail = user.getEmail();
assertThat(userEmail).isEqualTo(testId);
System.out.println("testId = " + testId);
System.out.println("userEmail = " + userEmail);
});
}
}
- 유닛테스트 결과
적용 결과
우선 결과가 저렇게 나온 것에 의문이 들었으나 원인은 이 부분인거 같다.
@Indexed
private String refreshToken;
- @RedisHash 어노테이션: 이 어노테이션은 클래스의 인스턴스를 레디스에 저장할 때 사용될 기본 키(prefix)를 정의한다. 여기서 value = "refreshToken"은 모든 RefreshToken 객체가 "refreshToken"으로 시작하는 키를 가지게 된다.
- @Id 어노테이션: @Id로 표시된 id 필드는 레디스에서 각 객체의 고유 식별자로 사용된다. 예를 들어, id 값이 "1223v@naver.com"인 객체는 "refreshToken:1223v@naver.com"이라는 키로 저장된다.
- @Indexed 어노테이션: @Indexed 어노테이션이 적용된 필드는 레디스에서 별도의 인덱스로 관리된다. 이 경우, refreshToken 필드는 검색을 위해 인덱스가 생성될 것이다.
생성된 키들의 의미:
- refreshToken:1223v@naver.com:idx: 이 키는 아마도 1223v@naver.com이라는 id를 가진 RefreshToken 객체의 인덱스 또는 특정 속성을 나타낸다.
- refreshToken:1223v@naver.com: 이 키는 id가 "1223v@naver.com"인 RefreshToken 객체를 나타낸다.
- refreshToken: 이 키는 클래스 레벨에서 사용되는 일반적인 설정 또는 메타데이터를 저장할 수 있다.
- refreshToken:refreshToken:testRefreshToken1: 이 키는 일반적인 패턴과 다르게 보이며, 특정 케이스 또는 테스트 데이터를 나타낼 수 있다. 클래스와 필드 이름이 키에 중복으로 포함되어 있는 것으로 보인다.
레디스(Redis)와 함께 사용되는 Spring Data Redis에서 @Indexed 어노테이션은 중요한 역할을 합니다.
이 어노테이션이 없으면, Spring Data Redis는 자동으로 생성된 인덱스를 사용하여 검색을 수행하지 않습니다.
즉, findByRefreshToken과 같은 메소드를 사용하여 특정 필드를 기준으로 객체를 찾을 때,
해당 필드에 @Indexed 어노테이션이 없으면 검색이 제대로 수행되지 않을 수 있습니다.
@Indexed 어노테이션은 해당 필드의 값을 기준으로 자동 인덱스를 생성하며,
이는 후에 repository의 쿼리 메소드들이 해당 필드를 효율적으로 검색할 수 있도록 합니다.
예를 들어, findByRefreshToken 메소드가 있고, refreshToken 필드에 @Indexed 어노테이션이 적용되어 있다면,
이 필드 값으로 객체를 빠르고 효율적으로 찾을 수 있습니다.
그러나 @Indexed 없이도 조회를 수행할 방법들이 있습니다.
예를 들어, 사용자 정의 쿼리를 작성하거나, 다른 방법으로 데이터를 구성하여 검색을 수행할 수 있습니다.
하지만 이런 방법들은 일반적으로 @Indexed를 사용하는 것보다 덜 효율적이거나 구현하기 복잡할 수 있습니다.
요약하자면, @Indexed 어노테이션은 Spring Data Redis에서 특정 필드를 기준으로 객체를 검색할 때 중요한 역할을 하며,
이 어노테이션이 없으면 특정 필드를 기준으로 한 검색이 예상대로 동작하지 않을 수 있습니다.
Spring Data Redis는 @RedisHash 어노테이션을 사용하여 객체를 저장할 때 특정한 형식의 키를 생성한다. 이 경우, RefreshToken 객체는 refreshToken:<id> 형식의 키로 저장된다. 여기서 <id>는 실제 객체의 id 값이다.
예를 들어, id가 "1234"인 RefreshToken 객체를 조회하려면, 레디스 키는 refreshToken:1234가 된다.
- 키 조회: HGETALL 명령어를 사용하여 객체의 모든 필드와 값을 조회한다. 예시 명령어는 HGETALL refreshToken:1234이다.
- HGETALL <refreshToken:1223v@naver.com>
이 명령어는 해당 키에 저장된 모든 해시 필드와 값을 반환한다. 만약 id 값이 "1234"인 RefreshToken 객체가 레디스에 저장되어 있다면, 해당 객체의 모든 필드와 값을 볼 수 있을 것이다.
또한, 객체의 특정 필드만 조회하고 싶다면 HGET 명령어를 사용할 수 있다. 예를 들어, refreshToken 필드만 조회하려면 다음 명령어를 사용한다:
HGET <refreshToken:1223v@naver.com> refreshToken