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
'Spring' 카테고리의 다른 글
Spring - 이메일 인증 구현해보기 (랜덤 인증번호 보내기) (9) | 2023.04.21 |
---|---|
Spring - 로컬 환경을 위한 Embedded Redis 적용하기 (+ Can't start redis server. Check logs for details) (3) | 2023.04.15 |
Spring - Spring Security + JWT 4편: Access Token 재발급 (2) | 2023.04.14 |
Spring - Spring Security + JWT 적용기 3편: 로그아웃 (3) | 2023.04.14 |
Spring - Spring Security + JWT 적용기 2편: JWT 검증 (0) | 2023.04.14 |