Spring Security를 사용한 사용자 인증 과정에서 Redis를 활용하려는 중에 다음과 같은 에러 문구를 확인할 수 있었다.
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.shineidle.tripf.user.entity.User
이 문제는 사용자의 인증 정보를 Redis에 저장하고 이를 다시 조회하는 과정에서 발생했다.
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL
);
template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));
return template;
}
이 설정을 통해 Redis에 저장되는 데이터가 JSON 형식으로 직렬화되며, 저장된 데이터를 역직렬화할 때도 올바른 객체 타입으로 변환된다.
사용자 정보를 Redis에서 우선적으로 조회하고, 없을 경우 데이터베이스에서 가져오도록 수정했다.
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User orFetchFromDB = redisUtils.getOrFetchFromDB(username, () -> loadUser(username), Duration.ofMinutes(3));
return new UserDetailsImpl(orFetchFromDB);
}
private User loadUser(String username) {
return this.userRepository.findByEmail(username)
.orElseGet(() -> this.userRepository.findByProviderId(username)
.orElseThrow(() -> new UsernameNotFoundException("유저를 찾을 수 없습니다.")));
}
redisUtils.getOrFetchFromDB 메서드를 사용하여 Redis에 사용자 정보가 없으면 DB에서 조회하고, 해당 데이터를 Redis에 저장하는 방식으로 동작한다. 또한, 조회된 사용자 정보를 UserDetails로 변환해 반환한다.
package com.shineidle.tripf.common.util;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
@RequiredArgsConstructor
public class RedisUtils {
private final RedisTemplate<String, Object> redisTemplate;
/**
* Redis에 데이터를 저장
* @param key Redis 키
* @param value 저장할 값
* @param ttl 만료 시간 (Duration)
*/
public void saveToRedis(String key, Object value, Duration ttl) {
redisTemplate.opsForValue().set(key, value, ttl);
}
/**
* Redis에서 데이터를 조회
* @param key Redis 키
* @return 조회한 값 (없으면 null)
*/
public Object getFromRedis(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* Redis에서 데이터를 조회하고 없으면 DB에서 가져옴
* @param key Redis 키
* @param dbFetcher DB에서 데이터를 조회하는 메서드
* @param ttl 만료 시간 (Duration)
* @return Redis 또는 DB에서 가져온 값
*/
public <T> T getOrFetchFromDB(String key, DbFetcher<T> dbFetcher, Duration ttl) {
T value = (T) redisTemplate.opsForValue().get(key);
if (value == null) {
value = dbFetcher.fetch();
saveToRedis(key, value, ttl);
}
return value;
}
// DB에서 데이터를 조회하는 메서드의 인터페이스
public interface DbFetcher<T> {
T fetch();
}
}
직렬화/역질렬화 문제가 해결되고 제대로 조회가 되는 모습을 확인할 수 있다.
유저를 조회할 때 최초에 Redis에서 조회를 하게 되는데 데이터가 없으므로 DB에서 가져와 Redis에 캐싱해 놓고 데이터를 반환해 응답해주고있다. (Cache Aside 방식)
스크린샷을 보면 최초에 쿼리가 수행되고 다음부터는 나오지않는 모습을 확인할 수 있다.
이 글은 개발자들한테 치킨 같은 존재네요. 언제나 옳고, 보면 기분 좋아지고, 다시 생각나고… 진짜 맛있습니다.