도입계기
토이프로젝트에서 JWT토큰인증과정을 도입하면서
인증이 필요한 요청마다 AUTHENTICATION 헤더를 불러와서
요청한 유저가 DB에 존재하는 유저인지 확인하는 과정이 필요했다.
매 요청마다 불필요한 IO가 발생하는 문제가 있어서 이를 캐싱을 통해서 해결해보았다.
개발환경
windows11
Intellij IDEA Ultimate
Spring Boot 3.2.1
Kotlin
JDK17
build.gradle
redis 의존성을 추가해준다.
dependencies {
//생략
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
//생략
}
RedisConfig
@Configuration
@EnableRedisRepositories
@RequiredArgsConstructor
class RedisConfig(
private val redisProperties: RedisProperties
) {
// 커넥션 정보 설정
@Bean
fun redisConnectionFactory(): RedisConnectionFactory {
val redisURI = RedisURI.Builder.redis(redisProperties.host, redisProperties.port).build()
val configuration = LettuceConnectionFactory.createRedisConfiguration(redisURI)
val factory = LettuceConnectionFactory(configuration)
factory.afterPropertiesSet()
return factory
}
// RedisTemplate 설정
@Bean
fun userRedisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<String, User> {
val redisTemplate: RedisTemplate<String, User> = RedisTemplate()
redisTemplate.connectionFactory = redisConnectionFactory
redisTemplate.keySerializer = StringRedisSerializer()
redisTemplate.valueSerializer = Jackson2JsonRedisSerializer(User::class.java)
return redisTemplate
}
}
UserCacheRepository
@Repository
@RequiredArgsConstructor
class UserCacheRepository(
private val redisTemplate: RedisTemplate<String, User>
) {
private val log = LoggerFactory.getLogger(javaClass)
fun setUser(user: User) {
val key = "USER:${user.userName}"
// USER_CACHE_TTL 은 최상위 함수로 Duration.ofDays(2) 을 의미한다.
// (캐시를 이틀동안 저장하겠다는 의미)
log.info("Set User to Redis {}, {}", key, user);
redisTemplate.opsForValue().set(key, user, USER_CACHE_TTL)
}
fun getUser(userName: String): User? {
val key = "USER:$userName"
val user = redisTemplate.opsForValue().get(key)
log.info("Get data from Redis {} , {}", key, user);
return user
}
}
준비가 되었다면 기존 JWT필터에서 유저정보를 가지고오는 부분을 수정한다.
기존 userService.loadUserByUserName(userName)
의 구현을
override fun loadUserByUserName(userName: String): User {
return userCacheRepository.getUser(userName // 캐시에서 데이터 조회후
?:getUserByUserNameOrException(userName) // Null이면 DB에서 조회 수행
}
로
userService.login()
override fun login(userName: String, password: String): String {
// 회원가입 여부 체크
val user: User = userCacheRepository.getUser(userName)
?:getUserByUserNameOrException(userName)
userCacheRepository.setUser(user); // 유저정보가 있다면 캐싱
// 비밀번호 체크
if(!passwordEncoder.matches(password, user.password)) {
throw SnsApplicationException(ErrorCode.INVALID_PASSWORD)
}
// 토큰 생성
return JwtUtils.generateToken(userName, secretKey, expiredMs)
}
이후 실제 API를 통해서 정상적으로 동작하는지 확인해본다.
로그인
로그인이 성공적으로 진행되었고,
redis에도 정상적으로 저장이 된 모습을 확인할 수 있다.
이후 인증이 필요한 API호출을 해본다.
미리 남긴 로그를 통해서
유저정보를 Redis에서 성공적으로 가지고 왔음을 확인할 수 있었고,
또한 캐싱에 성공한 데이터는 더이상 유저정보를 조회하는 DB IO가 사라졌다.