기존에는 서버의 DB(MySQL)에 RefreshToken을 보관해서 사용하는 방식을 많이 사용했었다. 하지만 MySQL의 부하를 줄이기 위해 보다 효율적인 저장 방식이 필요하다고 생각했다. 해당 프로젝트를 같이 하던 팀원들과 논의하던 중 Redis를 활용해보자는 의견이 나왔고, 이에 Redis의 장점을 조사하게 되었다.
✅ 메모리 기반 DB라서 초고속 성능 제공
✅ MySQL과 조합 & Key-Value 저장 방식으로 조회 성능 최적화 가능
✅ TTL(Time To Live) 기능 지원 → JWT 기반 세션 관리 최적화
Redis는 위와 같은 장점을 가지고 있었고 RefreshToken 특성상, 반복적이고 빠른 읽기 요구 & RefreshToken의 만료시간 관리가 필요했다. 결과적으로 NoSQL + In-Memory + expiretime
을 종합적으로 가지는 Redis가 토큰을 관리하는 데에 적합하다고 판단하여 해당 프로젝트에 적용하게 되었다.
기존의 프로젝트에서는 AccessToken만을 발급해 사용하고 있었는데, 여기에 RefreshToken을 발급하는 로직을 추가하고 이를 Redis에서 관리하게 하였다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
@Configuration
@EnableRedisRepositories
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);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
// 일반적인 key:value의 경우 시리얼라이저
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
// Hash를 사용할 경우 시리얼라이저
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
// 모든 경우
redisTemplate.setDefaultSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
return new LettuceConnectionFactory(host, port);
:LettuceConnectionFactory
를 사용하여 Lettuce 기반으로 Redis 연결을 하고 있다. Spring Boot에서는 기본적으로 Lettuce를 사용하며, 별도로 설정하지 않으면 Lettuce가 기본으로 선택된다. RedisTemplate<String, Object>
setDefaultSerializer()
를 사용하면 모든 데이터(key, value, hash key, hash value 등)에 동일한 직렬화 방식 적용 가능나의 경우, 가장 기본적인 1️⃣번 방식을 적용했다.
spring:
data:
redis:
host: localhost
port: 6379
기존에는 accessToken만 발급했던 로그인 로직에 refreshToken도 발급하고 저장할 수 있도록 수정했다.
1️⃣ JwtProvider
public String createRefreshToken(User user) {
if (user == null) {
throw new IllegalArgumentException("유저가 존재하지 않습니다.");
}
Key key = getSecretKey();
String refreshToken = Jwts.builder()
.signWith(key)
.header()
.add("typ", "JWT")
.add("alg", key.getAlgorithm())
.and()
.claim("id", user.getId())
.expiration(Date.from(Instant.now().plusMillis(refreshExpiration)))
.compact();
redisUtil.set(user.getEmail(), refreshToken);
redisUtil.expire(user.getEmail(), refreshExpiration, TimeUnit.MILLISECONDS);
// Redis에 저장된 값 확인을 위한 로그 출력
String storedToken = (String) redisUtil.get(user.getEmail());
if (storedToken != null) {
System.out.println("Redis에 저장된 refreshToken: " + storedToken);
} else {
System.out.println("Redis에 refreshToken이 저장되지 않았습니다.");
}
return refreshToken;
}
redisUtil.set(user.getEmail(), refreshToken);
를 통해 유저의 이메일과 refreshToken를 key-value 방식으로 저장했다. 127.0.0.1:6379> GET user@example.com
"abcd1234efgh5678"
redisUtil.expire(user.getEmail(), refreshExpiration, TimeUnit.MILLISECONDS);
를 통해 해당 유저의 이메일에 TTL(만료시간)을 설정하게 된다.
@Component
@RequiredArgsConstructor
public class RedisUtil {
private final RedisTemplate<String, Object> redisTemplate;
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
public boolean exists(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public void expire(String key, long timeout, TimeUnit unit) {
redisTemplate.expire(key, timeout, unit);
}
public boolean delete(String key) {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
public String extractEmailFromToken(String token) {
try {
return JWT.decode(token).getClaim("email").asString();
} catch (JWTDecodeException e) {
throw new IllegalArgumentException("유효하지 않은 JWT 토큰입니다.", e);
}
}
}
위와 같이 127.0.0.1:6379> keys *
를 통해 redis에 저장된 모든 키를 조회할 수 있다. 확인하고 싶은 사용자의 이메일이 user@example.com이라면 127.0.0.1:6379> GET user@example.com
를 통해 해당하는 refreshToken를 확인할 수 있다.
참고: https://velog.io/@cutepassions/Redis%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-JWT-%ED%86%A0%ED%81%B0-%EA%B4%80%EB%A6%AC-feat.%EC%BF%A0%ED%82%A4
github 코드: https://github.com/cholog-project/beacon/pull/68/files