이번에는 전편에서 h2 저장소 대신 redis를 사용해 보겠습니다! 실은 어차피 둘 다 인메모리 저장소이기 때문에 별 차이는 없을거라 생각합니다. 하지만 필자가 redis를 써보고 싶기도 하고, 보통은 디스크에 저장하는 데이터베이스보다 빠른 성능을 기대할 수 있습니다.
@Configuration
@EnableRedisRepositories
public class RedisConfig {
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.host}")
private String host;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
먼저 Redis를 세팅해줍니다. Redis connector는 두 종류가 있는데 Lettece와 jedis입니다. 공식문서를 보면 Lettece는 Netty-based 오픈 소스라고 설명되어 있습니다. jedis는 community-based 라고 되어있는데 무슨 말인지 잘 모르겠네요. 필자는 Lettece를 선택했는데 이유는 여기를 보고 참고했기 때문입니다 하하하;
public interface RefreshRedisRepository extends CrudRepository<RefreshRedisToken, String> {
}
redis를 쉽게 사용하고 hash 타입의 데이터를 저장하기 위해 repository 방식을 사용했습니다. 마치 jpa를 사용하는 것과 비슷한 방식이라서 금방 적응했던것 같습니다.
@Transactional(readOnly = true)
public TokenResponseDto reissueAccessToken(String token) {
//token 앞에 "Bearer-" 제거
String resolveToken = resolveToken(token);
//토큰 검증 메서드
//실패시 jwtTokenProvider.validateToken(resolveToken) 에서 exception을 리턴함
jwtTokenProvider.validateToken(resolveToken);
Authentication authentication = jwtTokenProvider.getAuthentication(resolveToken);
// 디비에 있는게 맞는지 확인
RefreshRedisToken refreshRedisToken = refreshRedisRepository.findById(authentication.getName()).get();
// 토큰이 같은지 확인
if(!resolveToken.equals(refreshRedisToken.getToken())){
throw new RuntimeException("not equals refresh token");
}
// 재발행해서 저장
String newToken = jwtTokenProvider.createRefreshToken(authentication);
RefreshRedisToken newRedisToken = RefreshRedisToken.createToken(authentication.getName(), newToken);
refreshRedisRepository.save(newRedisToken);
// accessToken과 refreshToken 모두 재발행
return TokenResponseDto.builder()
.accessToken("Bearer-"+jwtTokenProvider.createAccessToken(authentication))
.refreshToken("Bearer-"+newToken)
.build();
}
코드는 h2를 사용했을때와 유사합니다. 특이한 점은 redis를 사용하므로 @Transactional(readOnly = true)를 걸어도 잘 동작한다는 점과 redis는 key값으로 save 했을 경우 값이 이미 존재하면 덮어쓰기 하고 없으면 생성하는 특징이 있습니다.
@SpringBootTest
class RefreshRedisRepositoryTest {
@Autowired
private RefreshRedisRepository refreshRedisRepository;
@AfterEach
public void tearDown() throws Exception {
refreshRedisRepository.deleteAll();
}
@Test
void 기본_등록_조회기능() {
String id = "dyparkkk";
RefreshRedisToken token = RefreshRedisToken.builder()
.userId(id)
.token("token")
.build();
// when
refreshRedisRepository.save(token);
RefreshRedisToken findToken = refreshRedisRepository.findById(id).get();
assertThat(findToken.getToken()).isEqualTo("token");
}
@Test
void 수정기능() {
String id = "dyparkkk";
refreshRedisRepository.save(RefreshRedisToken.builder()
.userId(id)
.token("token")
.build());
//when
RefreshRedisToken findToken = refreshRedisRepository.findById(id).get();
findToken.reissue("new_token");
refreshRedisRepository.save(findToken);
//then
RefreshRedisToken refreshToken = refreshRedisRepository.findById(id).get();
assertThat(refreshToken.getToken()).isEqualTo("new_token");
}
}
Redis를 처음 써보기 때문에 간단한 테스트 코드를 작성하고 잘 되는지 테스트 해봤습니다.
잘 동작하는군요 !
refresh token과 access token을 재발급 받아봤습니다. 특이한 것은 h2가 redis보다 조금 더 빠르다는 것입니다.
h2 저장소 사용
5번정도 테스트 해봤는데 h2가 약간 더 빨랐습니다. 약 3ms정도...
물론 유의미한 속도차이는 아니라고 생각하고 오차범위 일수도 있습니다만, jpa가 최적화가 정말 잘 되어 있다고 짐작해 볼 수 있을것 같습니다.
Redis를 사용해본 이유는 Redis가 강력한 자원이라고 생각했기 때문이다. 파레토법칙을 가정하면 캐시 기능이 많은 성능 문제를 해결 할 수 있다. 또한 Redis는 다양한 데이터 구조를 지원하며, single thread라는 특징으로 트랜잭션 충돌을 걱정하지 않아도 된다. 정말 만능한 아키택처가 아닐 수 없다. (물론 그런 것은 없다)
다음에는 캐시기능을 중점적으로 Redis를 사용해면 좋을 것 같다.
참고자료 :
https://docs.spring.io/spring-data/redis/docs/current/reference/html/#redis.repositories
https://wildeveloperetrain.tistory.com/59
https://bcp0109.tistory.com/328
https://www.javainuse.com/webseries/spring-security-jwt/chap7