Redis Cache 이용한 성능 개선


CQRS와 Message Queue에 대해서 알아보면서

RabbitMQ를 이용해 요청에 대한 성능을 개선했었다.

https://hyerin6.github.io/2021-11-08/rabbitmq/

이후 SNS 특성상 Insert 요청보다 Read 요청이 훨씬 많기 때문에

어떻게 Cache를 적용할지 많은 고민을 했다.


특히 타임라인 조회는 내가 팔로우하고 있는 사용자들이 작성한 게시글을 조회하는 것이기 때문에

User, Post, Follow 테이블이 전부 관련되어 있고 N+1 문제 등을 해결하면서

Join 연산으로 타임라인을 조회하기로 결정했기 때문에 Cache를 적용하는게 적합한지 의문이었다.


우선 데이터의 변경이 적고 조회가 많은 특정 게시글 1개 조회, 특정 사용자의 게시글 목록에 적용했고

팔로우 목록, 댓글, 타임라인 조회에도 캐싱을 적용해 놓은 상태지만

테스트를 해보면서 캐싱하는게 적절한지 확인해 볼 필요가 있을 것 같다.


Spring Boot Redis Cache 적용

spring:
  redis:
    session:
      host: 
      port: 
      password: 
    cache:
      host: 
      port: 
      password:

JWT 토큰을 redis에 저장하고 있었기 때문에 session과 cache로 분리하여 설정했다.


@RequiredArgsConstructor
@Configuration
public class RedisConfig {

	@Value("${spring.redis.session.port}")
	private int redisPort;

	@Value("${spring.redis.session.host}")
	private String redisHost;

	@Value("${spring.redis.session.password}")
	private String redisPassword;

	@Bean
	public RedisConnectionFactory redisConnectionFactory() {
		RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
		redisStandaloneConfiguration.setPassword(redisPassword);
		redisStandaloneConfiguration.setHostName(redisHost);
		redisStandaloneConfiguration.setPort(redisPort);
		LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration);
		return lettuceConnectionFactory;
	}

	@Bean(name = "redisTemplate")
	public StringRedisTemplate redisTemplate() {
		StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
		stringRedisTemplate.setConnectionFactory(redisConnectionFactory());
		return stringRedisTemplate;
	}
}

Lettuce는 Netty (비동기 이벤트 기반 고성능 네트워크 프레임워크) 기반의 Redis 클라이언트이다.

비동기로 요청을 처리하기 때문에 Jedis에 비해 몇배 이상의 성능과 하드웨어 자원 절약이 가능하다.

자세한 정보는 이 블로그에 정리되어 있다.

https://jojoldu.tistory.com/418


@EnableCaching
@Configuration
public class CacheConfig {

	@Value("${spring.redis.cache.host}")
	private String redisHost;

	@Value("${spring.redis.cache.port}")
	private int redisPort;

	@Bean(name = "redisCacheConnectionFactory")
	public RedisConnectionFactory redisCacheConnectionFactory() {
		LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisHost,
			redisPort);
		return lettuceConnectionFactory;
	}

	@Bean
	public CacheManager cacheManager(
		@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
		RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder
			.fromConnectionFactory(redisConnectionFactory);
		RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
			.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
			.serializeValuesWith(
				RedisSerializationContext.SerializationPair.fromSerializer(
					new GenericJackson2JsonRedisSerializer(objectMapper())))
			.entryTtl(Duration.ofMinutes(3L));
		builder.cacheDefaults(configuration);
		return builder.build();
	}

	private ObjectMapper objectMapper() {
		PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator
			.builder()
			.allowIfSubType(Object.class)
			.build();
		ObjectMapper mapper = new ObjectMapper();
		mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
		mapper.registerModule(new JavaTimeModule());
		mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL);
		return mapper;
	}

}

ObjectMapper는 LocalDateTime 직렬화/역직렬화를 위해 추가한 bean이다.

모듈은 ObjectMapper에게 LocalDateTime으로 작업하는 방법을 가르치고,

매개변수 WRITE_DATES_AS_TIMESTAMPS는 JSON에서 LocalDateTime을 문자열로 표시하도록한다.

스프링 부트를 사용하면 ObjectMapper는 Bean 형태로 주입하여 제공한다.

이 설정이 없으면 다음 링크에서 말하는 에러와 동일한 에러가 발생한다.

https://stackoverflow.com/questions/27952472/serialize-deserialize-java-8-java-time-with-jackson-json-mapper


// 데이터를 조회할 때 레디스 캐시에 저장된다. 
@Cacheable(value = "post", key = "#id")
@Transactional(readOnly = true)
public Post getPost(Long id) throws ResponseException {
	return postRepository.findById(id)
		.orElseThrow(NotFoundException.POST);
}

// 데이터 변경이 있을 때 캐시가 삭제된다.
@CacheEvict(value = "post", key = "#id")
@Transactional
public void modify(Long id, ModifyPostRequest request) throws ResponseException {
	Post post = postRepository.findById(id)
		.orElseThrow(NotFoundException.POST);

	post.modifyContent(request.getContent());
}



Redis에 캐싱되는지 확인

redis-cli에 접속해서 확인해본 결과 원하는대로 캐싱되었다.

스크린샷 2021-11-11 오후 7 22 47

위와 같이 redis에 데이터가 들어간걸 확인할 수 있다.



캐시 적용 전후 조회 시간 차이

DB에서 조회 스크린샷 2021-11-11 오후 7 25 28


캐싱된 데이터 조회
스크린샷 2021-11-11 오후 7 25 47

postman으로 테스트해본 결과 조회하는데 소요되는 시간이 개선된 것을 확인할 수 있었다.