SpringBoot에서 Redis를 사용해 Object 캐시하기
이전 글에는 문자열에 대하여 @Cacheable, @CacheEvict을 사용해 redis에 캐싱하는 방법을 알아보았습니다.
이번 글에서는 문자열이 아닌 Object에 대하여 캐싱하는 방법에 대해 알아보겠습니다.
Model
캐싱의 대상 모델로 다음과 같은 User 클래스를 정의하겠습니다.
public class User {
private String id;
private String name;
private int age;
// getters, setters
}
@Cacheable, @CacheEvict 등록
get과 delete 메서드에 각각 @Cacheable, @CacheEvict 애노테이션을 등록하였습니다. get에서는 캐시 데이터를 생성, delete에는 캐시 데이터를 제거하게 될 것입니다.
@RestController
@RequestMapping("/redis")
public class RedisController {
private static final Logger logger = LoggerFactory.getLogger(RedisController.class);
@GetMapping()
@Cacheable(value = "user")
public User get(@RequestParam(value = "id") String id, @RequestParam(value = "name") String name, @RequestParam(value = "age") int age) {
logger.info("get user - userId:{}", id);
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return new User(id, name, age);
}
@DeleteMapping()
@CacheEvict(value = "user")
public void delete(@RequestParam(value = "id") String id, @RequestParam(value = "name") String name, @RequestParam(value = "age") int age) {
logger.info("delete user - userId:{}", id);
}
}
이제 localhost:8080/redis에 쿼리로 id, name, age를 담에 get 요청을 전달해봅시다. 시도했다면 기대했던 것과 달리 User객체를 직렬화하는 과정에서 에러가 발생했다는 메시지를 확인할 수 있을 것입니다.
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.changwoo.example.model.User]] with root cause
java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.changwoo.example.model.User] at...
객체의 직렬화를 위해 두가지 방법을 사용할 수 있습니다. 첫 번째는 User객체가 Serializable을 implements 하여 직렬화가능하게 만드는 방법입니다. 이 방법은 캐싱하려는 모든 모델이나 참조 모델에 연쇄적으로 선언되어야 합니다. 두 번째는 Spring에서 제공하는 Serializer를 CacheManager나 RedisTemplate에 등록하는 것입니다.
CacheManager 생성
Redis 캐싱을 위한 Config클래스와 CacheManager빈을 생성합니다. 앞선 프로젝트에서 Application에 붙여주었던 @EnableCaching도 목적에 맞게 해당 클래스로 옮겨줍시다.
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public CacheManager userCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofMinutes(3L));
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory).cacheDefaults(redisCacheConfiguration).build();
}
}
Spring은 @EnableCaching이 등록되어 있기에 AutoConfigurate시 RedisConnectionFactory를 생성합니다(RedisConnectionFactory를 상속한 LettuceConnectionFactory 자동생성). 그렇기에 userCacheManager 빈은 메서드 파라미터에서 RedisConnectionFactory빈을 자동 주입받을 수 있습니다.
Serialize관련 설정은 RedisCacheConfiguration에서 이루어 집니다. key직렬화에는 StringRedisSerializer를 등록하며, value직렬화에는 GenericJackson2JsonRedisSerializer를 등록해줍니다. 추가적으로 캐시의 TTL(유효기간)을 3분으로 지정해주었습니다.
마지막에 RedisCacheManagerBuilder가 connectionFactory와 configuration을 이용하여 CacheManager를 생성해줍니다.
캐시관련 애노테이션에 cacheManager전달
@Cacheable, @CacheEvict 애노테이션에 cacheManager빈을 등록합니다. 직렬화 관련한 설정들을 cacheManager가 갖고 있으니 다시 localhost:8080/redis로 get요청을 보내봅시다 (테스틀 위해 redis-server는 실행되고 있어야 합니다).
@RestController
@RequestMapping("/redis")
public class RedisController {
private static final Logger logger = LoggerFactory.getLogger(RedisController.class);
@GetMapping()
@Cacheable(value = "user", cacheManager = "userCacheManager")
public User get(@RequestParam(value = "id") String id, @RequestParam(value = "name") String name, @RequestParam(value = "age") int age) {
logger.info("get user - userId:{}", id);
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return new User(id, name, age);
}
@DeleteMapping()
@CacheEvict(value = "user", cacheManager = "userCacheManager")
public void delete(@RequestParam(value = "id") String id, @RequestParam(value = "name") String name, @RequestParam(value = "age") int age) {
logger.info("delete user - userId:{}", id);
}
}
첫 번째 시도에는 응답에 2.52s가 소요되었습니다. Thread.sleep(1500)으로 걸어준 시간이 반영된 모습입니다.
redis에도 잘 반영된 모습입니다. 이제 다시 요청을 보내 캐싱된 속도를 확인하도록 하겠습니다.
두 번째 시도에는 응답에 40ms가 소요되었습니다. 역시 캐싱이되어있어 get 메서드의 Thread.sleep(1500)이 수행되지 않고 캐싱된 값으로 응답을 내려주는 것을 확인할 수 있습니다.
@CacheEvict가 선언된 delete 메서드로 요청을 보낸다면, 해당 user에 대한 캐시 데이터가 삭제되는것을 확인할 수 있습니다.
지금까지 SpringBoot 프로젝트에서 Redis에 Object 데이터를 캐시할때 생기는 문제와 해결 과정 그리고 사용 방법에 대하여 알아보았습니다. 현재 예시에서는 @CacheEvict에서 캐시 데이터 삭제 시 @Cacheable에서 전달받은 파라미터인 id와 name, age를 모두 전달해줍니다. 하지만 캐시 삭제를 위해 사용자의 모든 필드 정보를 파라미터로 받는 행위는 매우 불필요하고 비경제적으로 보입니다. 그렇다고 파라미터를 제거한 후 실행하면 캐시가 지워지지 않는 것을 확인할 수 있는데요, 다음 게시글에서 이 문제를 해결하는 방법에 대하여 알아보겠습니다. 감사합니다.