본문 바로가기

Redis

Spring에 Redis적용하기

1. 라이브러리 설정하기

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

2. RedisConfig 생성하기

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean // RedisTemplate<String, Object>를 빈으로 등록하여 애플리케이션에서 사용할 수 있게 함
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        template.setKeySerializer(new StringRedisSerializer()); // key 값을 String으로 저장
        template.setValueSerializer(new GenericToStringSerializer<>(String.class)); // value 값을 String으로 저장
        return template;
    }

    @Bean
    public CacheManager productCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 응답 타입 Object 직렬화
        RedisCacheConfiguration objectCacheConfig = RedisCacheConfiguration
                .defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                        new Jackson2JsonRedisSerializer<>(Object.class)))
                .entryTtl(Duration.ofMinutes(30L));

        // 응답 타입 Response 직렬화
        RedisCacheConfiguration responseCacheConfig = RedisCacheConfiguration
                .defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                        new Jackson2JsonRedisSerializer<>(Response.class)));

        // 캐시 이름별로 직렬화 방식 지정
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        cacheConfigurations.put("rank", objectCacheConfig);
        cacheConfigurations.put("search", responseCacheConfig);

        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .withInitialCacheConfigurations(cacheConfigurations)
                .build();
    }
}

 

3. yml로 포트 설정해주기

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/freship
    username: root
    password: 
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        use_sql_comments: true
        dialect: org.hibernate.dialect.MySQLDialect
  data:
    redis:
      host: localhost
      port: 6379

 

4. redisTemplate을 사용하여 Redis에서 데이터 입출력하기

@Component
@RequiredArgsConstructor
public class ProductRedisUtils {

    private final RedisTemplate<String, Object> redisTemplate;

    // 사용자 검색 기록 저장 (동일한 검색어 중복 방지)
    public void saveSearchHistory(Long userId, String searchKeyword) {
        String searchHistoryKey = "user:" + userId + ":search:" + searchKeyword;
        if (Boolean.FALSE.equals(redisTemplate.hasKey(searchHistoryKey))) {
            redisTemplate.opsForValue().set(searchHistoryKey, "searched");
            incrementSearchRank(searchKeyword);
        }
    }

    // 검색어 순위 증가
    private void incrementSearchRank(String searchKeyword) {
        redisTemplate.opsForZSet().incrementScore("rank", searchKeyword, 1);
    }

    // 인기 검색어 조회 (상위 10개)
    public List<String> getTopSearchKeywords() {
        Set<String> popularSearch = redisTemplate.opsForZSet()
                .reverseRange("rank", 0, 9)
                .stream()
                .map(Object::toString)
                .collect(Collectors.toSet());
        return popularSearch != null ? new ArrayList<>(popularSearch) : new ArrayList<>();
    }

    public void setReadCount(Long id){
        redisTemplate.opsForZSet().add("product:readCount", "product:" + id, 1);
    }

    public Long getReadCount(Long productId){
        return redisTemplate.opsForZSet().score("product:readCount", "product:" + productId).longValue();
    }

    public Long addReadCount(Long productId){
        return redisTemplate.opsForZSet().incrementScore("product:readCount", "product:" + productId, 1).longValue();
    }

    public boolean notExistsReadCount(Long productId){
        // 값이 존재하지 않는다면 null 반환
        Double currentReadCount = redisTemplate.opsForZSet().score("product:readCount", "product:" + productId);
        return currentReadCount == null;
    }

    // 조회수 상위 10개의 상품 id 리스트 조회
    public List<Long> findProductIds() {
        Set<Object> objectSet = redisTemplate.opsForZSet().reverseRange("product:readCount",0, 9);
        if (objectSet == null) {
            return Collections.emptyList();
        }

        //"product:7" -> ["product", "7"] -> ["7"]로 변환
        List<Long> productIdList = new ArrayList<>();
        for (Object o : objectSet) {
            String value = (String) o;
            String[] parts = value.split(":");
            productIdList.add(Long.parseLong(parts[1]));
        }
        return productIdList;
    }

    public Boolean isNotViewed (Long productId, Long userId) {
        Boolean isNotViewed = redisTemplate.opsForValue().setIfAbsent("product:" + productId + ":user:" + userId, "viewed");
        return isNotViewed;
    }

    // 자정에 캐시 리셋
    @Scheduled(cron = "0 0 0 * * ?")
    public void clearCache() {

        Set<String> cacheKeys = redisTemplate.keys("search*");
        if (cacheKeys != null && !cacheKeys.isEmpty()) {
            redisTemplate.delete(cacheKeys);
        }

        // 유저 검색 기록 삭제
        Set<String> keysForDelete = redisTemplate.keys("user:*");
        if (keysForDelete != null && !keysForDelete.isEmpty()) {
            redisTemplate.delete(keysForDelete);
        }

        // 검색 조회수 삭제
        if (Boolean.TRUE.equals(redisTemplate.hasKey("rank"))) {
            redisTemplate.opsForZSet().removeRange("rank", 0, -1);
        }

        Set<String> visitorKeys = redisTemplate.keys("product*");
        if (visitorKeys != null && !visitorKeys.isEmpty()) {
            redisTemplate.delete(visitorKeys);
        }

        if (Boolean.TRUE.equals(redisTemplate.hasKey("product:readCount"))) {
            redisTemplate.opsForZSet().removeRange("product:readCount", 0, -1);
        }
    }

}

 

5. 캐싱하고 싶은 데이터에 어노테이션 붙이기

@Cacheable(cacheNames = "rank", key = "'products:rank'", cacheManager = "productCacheManager")

'Redis' 카테고리의 다른 글