Spring에서 @Cacheable
어노테이션을 이용하면 한 함수에서 같은 인자가 들어왔을 때, return 값을 caching 할 수 있다는 것은 대부분 아는 사실입니다.
하지만 가끔은 로직상에서 캐싱을 해야하는 경우도 있고, 꼭 return 값만을 caching해야하는 경우가 아닐 때도 있습니다.
또, 단순 string value 를 redis에 저장하는 경우도 있지만 object를 저장하고 싶은 경우도 있을텐데요. 이럴 때, redis를 어떻게 사용할 수 있을지 알아보겠습니다.
redis에 객체를 저장 할 때는, serializer를 통해 직렬화해주어야한다.
이 때, 선택할 수 있는 방법은 여러가지가 있는데 한 번 알아보자.
이 Serializer는 객체의 클래스 지정없이 직렬화해준다는 장점을 가지고있다. 하지만, 단점으로는 Object의 Class 및 package까지 전부 함께 저장하게 되어 다른 프로젝트에서 redis에 저장되어 있는 값을 사용하려면 package까지 일치시켜줘야하는 큰 단점이 존재한다.
따라서 MSA 관점의 프로젝트에서는 사용하지 않는 것이 좋고, 만약 프로젝트에 변경사항이 자주 발생한다면 그 때도 문제가 생길 수 있으니 사용을 추천하지 않는다.
이 serializer는 클래스를 지정해야해서, redis에 객체를 저장할 때 class 값을 저장하지 않는다.
따라서, pacakge 등이 일치할 필요가 없다는 장점이 있다.
하지만, class 타입을 지정하기 때문에 redisTemplate을 여러 쓰레드에서 접근하게 될 때 serializer 타입의 문제가 발생하는 경우가 발생한다.
따라서 이것도 사용하지 않을 것이다.
이름을 보면 알 수 있듯이, string 값을 그대로 저장하는 serilaizer이다.
이것을 사용하게 되면, JSON 형태로 직접 encoding, decoding을 해줘야한다는 단점이 있지만 위의 두 개의 serializer에서 발생할 수 있는 문제가 발생하지 않는다.
위 3개의 장점이 JSON을 직접 파싱하는 것보다 더 이익이 크기 때문에, string redis serializer를 사용하자.
설정은 다음과 같이 하면 된다.
@Bean
public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
이제 String serializer를 사용하게 되었으니, 파싱을 해야한다.
이건 간편하다.
아래와 같이 Object -> JSON, JSON -> Object를 위한 util 클래스를 하나 만들어 두면, 편리하게 사용할 수 있다.
public <T> boolean saveData(String key, T data) {
try {
ObjectMapper mapper = new ObjectMapper();
String value = objectMapper.writeValueAsString(data);
redisTemplate.opsForValue().set(key, value);
return true;
} catch(Exception e){
log.error(e);
return false;
}
}
public <T> Optional<T> getData(String key, Class<T> classType) {
String value = redisTemplate.opsForValue().get(key);
if(value == null){
return Optional.empty();
}
try {
ObjectMapper mapper = new ObjectMapper();
return Optional.of(objectMapper.readValue(value));
} catch(Exception e){
log.error(e);
return Optional.empty();
}
}
위 코드에서는 Exception을 swallow하고 log를 남기고 있는데, 필요에 따라 구체적인 Exception을 throw하는 방식으로 사용해도 좋을 것 같습니다.
엔티티 자체를 JSON으로 직렬화 할 때는 JPA를 사용할 시, 양방향 순환참조가 발생하여 stack overflow를 발생시키는 경우가 자주 있습니다.
이를 해결하기 위해서는 DTO를 사용한다거나, @JsonIgnore
를 사용하는 방법이 존재합니다.
하지만 위보다 간편하게 사용하는 방법이 하나 있는데요, 바로 @JsonIdentityInfo
을 통해 순환참조될 대상을 id로 구분할 수 있게 하는 것입니다.
@Id
어노테이션이 붙은 프로퍼티의 타입에 따라 엔티티 클래스에 해당 어노테이션을 다음과 같이 붙여주면 됩니다.
@JsonIdentityInfo(generator = UUIDGenerator::class, property = "id")
@JsonIdentityInfo(generator = StringIdGenerator::class, property = "id")
@JsonIdentityInfo(generator = IntSequenceGenerator::class, property = "id")
redis를 cache 어노테이션으로 사용해보다가 이번에 처음으로 이런 방식으로 사용해보았는데요, 저는 두 가지 장점들을 느꼈습니다.
이러한 장점을 이용하여, 개발에 직접 적용해 볼 수 있었고 잘 사용중입니다.
여러분도 한 번 적절한 이유가 있다면, redis를 이와 같이 사용해보는 것을 추천드립니다.
도움 되었습니다~ 고마워요