[Spring + Redis] Redis cache를 적용해 조회 성능 개선 방법

내 프로젝트에서는 사용자별로 사이드바에 찜 수, 장바구니 수들을 카운트 쿼리로 반환된 데이터 수를 view에 뿌려주는데,

 사이드바는 여러 페이지에서 노출되어 페이지 이동마다 카운트 쿼리를 자주 호출하고 있었다.

그렇기 때문에 매번 카운트 쿼리를 호출하지 않고 해당 데이터를 캐시 처리하여 카운트 수가 변경되지 않을 경우에 Redis 캐시에서 해당 데이터 수를 반환하도록 하여 DB의 부담을 줄이도록 하였다.

 

레디스에 대해 알고 싶다면 아래 포스팅에서 설명한다.

 

[Redis] 레디스란? 특징, 활용예시, 비교 정리

레디스란? Redis는 Remote Disctionary Server의 약자로 키(Key) : 값(Value) 해시 맵과 같은 구조를 가진 NoSQL 데이터베이스이다. MySQL, Oracle같은 일반 DB와 다르게 하드 디스크(SSD, HDD)에서 데이터를 처리 하는

hstory0208.tistory.com

 


Redistemplate 방식과 Redis repository 방식 비교

Redis 데이터에 접근하기 위한 방식으로 Spring에서 제공하는, Redistemplate 방식과 Redis repository 방식이 있다.

각각의 특징과 사용 시점을 비교하면 다음과 같다.

 

Redistemplate 
  • Key, Value 연산을 직접 수행할 수 있으며, String, Hash, List 등 Redis의 데이터 구조에 직접 접근 가능.
  • 세밀한 제어가 필요한 경우나 특정 Redis 명령을 사용해야 할 때 유용.
  • 사용자 정의 직렬화 전략을 적용하거나 복잡한 쿼리를 작성해야 하는 경우에 적합.

 

Redis repository
  • JPA와 유사하게 동작하여 CrudRepository<>를 상속받는 인터페이스를 생성하면 기본적인 CRUD 메서드를 사용할 수 있다.
  • 인터페이스만 정의하면 되므로 더 간편하게 구현 가능하다.
  • 복잡한 쿼리나 세밀한 제어가 필요할 경우에는 RedisRepository가 지원하지 않는 기능이 있을 수 있다.
  • 간단하고 일관된 CRUD 작업이 주된 작업의 경우에 적합하다.

Redistemplate 방식 설정

 

build.gradle 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

application.properties 설정
# Redis Setting
redis.host=localhost
redis.port=6379
spring.cache.type=redis

 

Redis 설정 클래스 생성
@Configuration
public class RedisConfig {

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

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<?, ?> redisTemplate() {
        RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

 

Redis 캐시 설정 클래스 생성
@Configuration
@EnableCaching
public class RedisCacheConfig {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory cf) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .entryTtl(Duration.ofMinutes(3L)); // 캐쉬 저장 시간 3분 설정

        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(cf)
                .cacheDefaults(redisCacheConfiguration)
                .build();
    }
}

어노테이션으로 캐싱 적용

RedisRestTemplate 방식으로 설정했다면, CRUD 작업을 하기 위한 서비스 클래스를 따로 작성하는 것이 필요하다.

하지만 현재 내가 적용하고 캐시를 적용하고 싶은 서비스에는 기존 서비스 로직안에 Redis에 CRUD하는 로직이 필요하진 않다.

 

그래서 이번 포스팅에서 캐싱을 적용하는건 메서드 수준에서 캐시를 적용해주는 @Cacheable과 @CacheEvict 어노테이션을 사용할 것이다.

해당 어노테이션을 사용하면 아주 간단하게 캐싱을 적용하고 제거 할 수 있다.

 

LikeService ( 캐시를 적용할 기존에 만들어놨던 서비스 )

다른 메서드에도 캐시를 적용하고 싶으면 아래 방식대로 캐시를 적용할 메서드에 해당 어노테이션을 적용해주면 된다.

@Service
@Transactional
@RequiredArgsConstructor
public class LikeService {
    private final LikeRepository likeRepository;
    public final static String LIKE_COUNT = "likeCount";

    @CacheEvict(cacheNames = LIKE_COUNT, key = "#like.userIdNo", cacheManager = "cacheManager")
    public void insertLike(Like like) {
        likeRepository.insertLike(like);
    }


    @CacheEvict(cacheNames = LIKE_COUNT, key = "#userId", cacheManager = "cacheManager")
    public void deleteLike(Long userId, Long id) {
        likeRepository.deleteLike(userId, id);
    }

    @Cacheable(cacheNames = LIKE_COUNT, key = "#id", condition = "#id != null", cacheManager = "cacheManager")
    public int countLikesByUserId(Long id) {
        return likeRepository.countLikesByUserId(id);
    }
}

 

사용된 캐시 전략

읽기 전략 : 캐시를 먼저 확인하고, 캐시에 데이터가 없을 경우 DB에서 데이터를 가져오는 Look Aside 전략 사용.

쓰기 전략 : 데이터를 업데이트 할 대마다 바로 캐시에 반영하지 않고, 변경된 내용만 DB에 반영하고 해당 캐시는 제거 또는 무효화하는 Write Around 전략 사용.

=> 캐시 전략에 대해 알아보기

 

@Cacheable

읽기 작업을 수행하는 메서드에 사용되는 어노테이션이다.

해당 어노테이션은 메서드의 특정 인자에 대한 메서드 결과값을 캐시 저장소에 key값으로 저장하고 동일한 메서드 결과값은 캐시 저장소에서 가져와 반환한다.

옵션 설명
cacheNames 저장될 캐시 이름을 설정한다. (필자는 상수로 뽑아서 지정했다.)
key Spring 표현식을 사용하여 동적으로 달라지는 메서드의 파라미터 값을 선언한다.
cacheManager  위에서 작성한 캐시 설정 클래스의 메서드 이름을 사용한다.
condition Spring 표현식을 사용해 해당 조건을 만족할 때만 캐시에 저장한다.

 

@CacheEvict

저장된 캐시를 제거할 때 사용된다. 여기서는 키 값을 지정해 해당 키 값만 캐시에서 제거하도록 하였다.

사용된 옵션은 위의 옵션에 대한 설명과 동일하다.

 

위에서 설명한대로 여기서 사용된 캐시 전략은 Look Aside + Write Around 조합이 사용되었다.

그렇기 때문에 DB에 저장된 데이터가 수정, 추가, 삭제 될 때마다 Cache 또한 삭제해주어야 한다.

만약 데이터가 변경될 때마다 캐시를 비워주지 않으면

캐시 DB(Redis)와 하드 DB(MySQL)의 데이터가 서로 다른 데이터를 갖고 있어 캐시DB에서 데이터를 꺼내 올 때 변경되기 전인 오래된 정보를 사용하는 데이터 정합성 문제점이 발생한다.

그렇기 때문에 데이터가 변경 될 때마다 캐시를 제거해주는 작업이 중요하다.

키 값은 cacheName::key 로 구성되어 있다.