Redis

SpringBoot에서 Redis를 사용해 Object 캐시하기

긍.응.성 2020. 11. 2. 01:48
반응형

redis logo

이전 글에는 문자열에 대하여 @Cacheable, @CacheEvict을 사용해 redis에 캐싱하는 방법을 알아보았습니다.

 

Redis와 Springboot 연결하기

앞선 글에서 Redis 설치 및 실행하는 과정에 대하여 알아보았습니다. Redis 설치 및 실행하기 앞선 글에서 Redis가 무엇인지에 대해 간략하게 알아보았습니다. Redis 란? What is Redis? Redis는 Remote Dictiona.

ckddn9496.tistory.com

이번 글에서는 문자열이 아닌 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를 모두 전달해줍니다. 하지만 캐시 삭제를 위해 사용자의 모든 필드 정보를 파라미터로 받는 행위는 매우 불필요하고 비경제적으로 보입니다. 그렇다고 파라미터를 제거한 후 실행하면 캐시가 지워지지 않는 것을 확인할 수 있는데요, 다음 게시글에서 이 문제를 해결하는 방법에 대하여 알아보겠습니다. 감사합니다.

 

반응형