[Spring] Redis에서 객체 그래프를 유지하며 "직접" 캐싱하기

kshired·2022년 5월 17일
3

Spring

목록 보기
9/11
post-thumbnail

Spring에서 @Cacheable 어노테이션을 이용하면 한 함수에서 같은 인자가 들어왔을 때, return 값을 caching 할 수 있다는 것은 대부분 아는 사실입니다.

하지만 가끔은 로직상에서 캐싱을 해야하는 경우도 있고, 꼭 return 값만을 caching해야하는 경우가 아닐 때도 있습니다.

또, 단순 string value 를 redis에 저장하는 경우도 있지만 object를 저장하고 싶은 경우도 있을텐데요. 이럴 때, redis를 어떻게 사용할 수 있을지 알아보겠습니다.

Serializer 설정하기

redis에 객체를 저장 할 때는, serializer를 통해 직렬화해주어야한다.

이 때, 선택할 수 있는 방법은 여러가지가 있는데 한 번 알아보자.

GenericJackson2JsonRedisSerializer

이 Serializer는 객체의 클래스 지정없이 직렬화해준다는 장점을 가지고있다. 하지만, 단점으로는 Object의 Class 및 package까지 전부 함께 저장하게 되어 다른 프로젝트에서 redis에 저장되어 있는 값을 사용하려면 package까지 일치시켜줘야하는 큰 단점이 존재한다.

따라서 MSA 관점의 프로젝트에서는 사용하지 않는 것이 좋고, 만약 프로젝트에 변경사항이 자주 발생한다면 그 때도 문제가 생길 수 있으니 사용을 추천하지 않는다.

Jackson2JsonRedisSerializer

이 serializer는 클래스를 지정해야해서, redis에 객체를 저장할 때 class 값을 저장하지 않는다.

따라서, pacakge 등이 일치할 필요가 없다는 장점이 있다.

하지만, class 타입을 지정하기 때문에 redisTemplate을 여러 쓰레드에서 접근하게 될 때 serializer 타입의 문제가 발생하는 경우가 발생한다.

따라서 이것도 사용하지 않을 것이다.

StringRedisSerializer

이름을 보면 알 수 있듯이, string 값을 그대로 저장하는 serilaizer이다.

이것을 사용하게 되면, JSON 형태로 직접 encoding, decoding을 해줘야한다는 단점이 있지만 위의 두 개의 serializer에서 발생할 수 있는 문제가 발생하지 않는다.

  1. class 타입의 지정이 필요하지 않다.
  2. package까지 일치할 필요가 없다.
  3. 쓰레드간의 문제가 발생하지 않는다.

위 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;
}

JSON parsing

이제 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 어노테이션이 붙은 프로퍼티의 타입에 따라 엔티티 클래스에 해당 어노테이션을 다음과 같이 붙여주면 됩니다.

  • uuid의 경우
    • @JsonIdentityInfo(generator = UUIDGenerator::class, property = "id")
  • string의 경우
    • @JsonIdentityInfo(generator = StringIdGenerator::class, property = "id")
  • number의 경우
    • @JsonIdentityInfo(generator = IntSequenceGenerator::class, property = "id")

마치면서

redis를 cache 어노테이션으로 사용해보다가 이번에 처음으로 이런 방식으로 사용해보았는데요, 저는 두 가지 장점들을 느꼈습니다.

  1. 객체를 redis에 저장하여 빠른 조회 가능 ( 너무나 당연한 장점 )
  2. redis에 저장한 json을 ObjectMapper로 parsing하게 되면 따로 설정 없이 객체 그래프 탐색 가능.

이러한 장점을 이용하여, 개발에 직접 적용해 볼 수 있었고 잘 사용중입니다.

여러분도 한 번 적절한 이유가 있다면, redis를 이와 같이 사용해보는 것을 추천드립니다.

references

profile
글 쓰는 개발자

2개의 댓글

comment-user-thumbnail
2023년 5월 7일

도움 되었습니다~ 고마워요

답글 달기
comment-user-thumbnail
2023년 8월 18일

ObjectMapper를 일일이 사용하는 게 귀찮고 코드가 지저분해지는 게 싫어서 GenericJackson2Json을 써볼까 하고 검색해서 들어왔는데, MSA 관점에서 다른 서버가 참조하려면 package 명 때문에 번거로워지는 단점이 존재했군요. 배우고 갑니다. 좋은 글 써주셔서 감사합니다.

답글 달기