이번 프로젝트에서는 인증/인가를 JWT를 통해 진행하였다. 초반 구현 단계에서는 MariaDB 내에 RefreshToken 테이블을 추가하여 리프레시 토큰을 관리하였지만 이후 Redis로 토큰 저장소를 변경하여 적용하였다.
기존 RDBMS 토큰 저장소에서의 변경 이유
Redis로 리프레시 토큰 저장소를 변경한 가장 큰 이유는 TTL이었다. 리프레시 토큰의 경우 생성 시 만료 시간을 설정해주는데, RDBMS 사용 시 스케쥴러를 통해 수동으로 DB 내부 리프레시 토큰 값을 삭제시켜주어야 했기에 Redis의 TTL(Time To Live) 기능을 활용하여 Redis 내에서 자동으로 리프레시 토큰을 삭제하도록 해주었다.
추가적으로, Redis는 인메모리 저장 방식을 사용하기에 응답 속도가 RDBMS 보다 10배 이상 빠르기에, 병목 발생 가능성을 낮춰준다.
Redis 적용
Redis Cloud Console & Redis Insight
로컬 Redis에서 로직 테스트 이후, 클라우드 환경에서의 Redis 연결을 위해 Redis Cloud Console을 사용하였다. 무료로 Memory usage 30MB 까지 사용이 가능하였다.
DBeaver에서 Redis에 대해서는 유료 사용자만 서비스를 제공했기에, Redis Insight를 GUI로 사용하였다.
build.gradle
gradle 파일에 redis implementation을 추가해주었다.
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
application.yml
Redis Cloud Console에서 제공하는 Public endpoint를 통해 host와 port를 입력, password 까지 설정해주었다.
spring:
data:
redis:
host: redis-*****.****.ap-northeast-*-*.ec2.redns.redis-cloud.com
port: *****
password: *****
RedisConfig 설정
yml 파일에서 설정한 설정 변수를 토대로 Redis 연결을 설정하고 RedisTemplate를 구성하였다.
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.password}")
private String password;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(host, port);
redisStandaloneConfiguration.setPassword(password);
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
RefreshTokenRepository 인터페이스
리프레시 토큰 존재 여부 확인, 저장, 삭제 메소드를 정의하였다.
public interface RefreshTokenRepository {
boolean existsByRefresh(String refresh);
void deleteByRefresh(String refresh);
void save(RefreshEntity refreshEntity);
}
RelationalRefreshTokenRepository 구현체
Redis Repository 적용 이전에, 기존에 JpaRepository를 사용하던 것에서 RefreshTokenRepository 인터페이스를 구현하는 방식으로 구조를 바꾸어 언제든 RDBMS로 변경할 수 있도록 구현해두고 싶었다. 이를 위해 어댑터 패턴을 통해 JpaRepository의 메소드들을 RefreshTokenRepository 인터페이스의 메소드와 대응하여 연결해주었다.
public class RelationalRefreshTokenRepository implements RefreshTokenRepository {
private final JpaRefreshTokenRepository jpaRefreshTokenRepository;
@Autowired
public RelationalRefreshTokenRepository(JpaRefreshTokenRepository jpaRefreshTokenRepository) {
this.jpaRefreshTokenRepository = jpaRefreshTokenRepository;
}
@Override
public boolean existsByRefresh(String refresh) {
return jpaRefreshTokenRepository.existsByRefresh(refresh);
}
@Override
public void deleteByRefresh(String refresh) {
jpaRefreshTokenRepository.deleteByRefresh(refresh);
}
@Override
public void save(RefreshEntity refreshEntity) {
jpaRefreshTokenRepository.save(refreshEntity);
}
}
RedisRefreshTokenRepository 구현체
리프레시 토큰 값에 prefix를 앞에 붙여 key 값으로 사용하고, expire() 메소드를 통해 TTL을 설정해주었다.
블랙 리스트 등 추가적인 기능 확장을 열어두기 위하여 email 값을 value로 저장하였다.
@Repository
@Primary
public class RedisRefreshTokenRepository implements RefreshTokenRepository {
private final RedisTemplate<String, Object> redisTemplate;
private static final String KEY_PREFIX = "refresh_token:";
public RedisRefreshTokenRepository(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public boolean existsByRefresh(String refresh) {
return Boolean.TRUE.equals(redisTemplate.hasKey(KEY_PREFIX + refresh));
}
@Override
public void deleteByRefresh(String refresh) {
redisTemplate.delete(KEY_PREFIX + refresh);
}
@Override
public void save(RefreshEntity refreshEntity) {
String key = KEY_PREFIX + refreshEntity.getRefresh();
redisTemplate.opsForValue().set(key, refreshEntity.getEmail());
if (refreshEntity.getExpiration() != null) {
Duration duration = Duration.between(LocalDateTime.now(), refreshEntity.getExpiration());
redisTemplate.expire(key, duration);
}
}
}
마치며
리프레시 토큰이라는 특정 형식의 도메인에 알맞은 저장 방식인 Redis을 사용하는 것은 좋은 경험이었다. 이후에 세션 기반 인증 방식으로 프로젝트를 진행하게 된다면 그때에도 Redis를 적용하고 싶다. 그리고 Redis Insight UI가 생각보다 너무 깔끔해서 좋았다..
