springboot + redis @Cacheable 사용

2024. 1. 11. 15:48·개발/java

redis @cacheable를 써보자

 

환경은 다음과 같다

  • springboot 2.7.18
  • jdk 11.0.21

잘 변하지 않는 데이터에 대해서 @Cacheable을 통해 DB , API호출이나 로직 수행을 하지 않고, redis cache에 저장된 값으로 즉각적으로 응답을 줄 수 있다.

 

springboot redis 기본 구성은 이전 게시물을 참조하자

https://fullmooney.tistory.com/27

 

springboot + redis 사용하기

redis를 써보자 환경은 다음과 같다 springboot 2.7.18 jdk 11.0.21 먼저 pom 에 dependency를 추가한다. org.springframework.boot spring-boot-starter-data-redis io.lettuce lettuce-core redis와 redisinsight 로컬 환경 구성(windows 기준

fullmooney.tistory.com

 

RedisConfig에 이어 CacheConfig를 추가로 설정한다.

org.springframework.cache.annotation.CachingConfigurerSupport 를 상속받고 @EnableCaching 을 Configuration 클래스에 추가한다.

아래 설정을 통해 @Cacheable을 통해 redis에 등록된 캐시는 prefix로 com이 붙게 되며 cache name과 추가 key를 append 하여 com::<CacheName>::<key> 형태의 Redis Key를 갖게 된다.

//RedisCacheConfig.java
import java.time.Duration;
import java.util.Map;
import java.util.TreeMap;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.CacheKeyPrefix;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.StringUtils;

import com.dev.common.handler.CacheExceptionHandler;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@EnableCaching
@Configuration
public class RedisCacheConfig extends CachingConfigurerSupport {

    public static final String CKEY = "CKEY";

    private final ObjectMapper objectMapper;
    private final RedisConnectionFactory redisConnectionFactory;


    private CacheKeyPrefix cacheKeyPrefix;

    @Value("${spring.cache.redis.key-prefix:com}")
    private String springCacheRedisKeyPrefix;

    @Value("${spring.cache.redis.use-key-prefix:true}")
    private boolean springCacheRedisUseKeyPrefix;


    public RedisCacheConfig(ObjectMapper objectMapper, RedisConnectionFactory redisConnectionFactory) {
        this.objectMapper = objectMapper;
        this.redisConnectionFactory = redisConnectionFactory;
    }


    @PostConstruct
    private void onPostConstructor() {
        if (springCacheRedisUseKeyPrefix && StringUtils.hasText(springCacheRedisKeyPrefix)) {
            cacheKeyPrefix = cacheName -> springCacheRedisKeyPrefix.trim() + "::" + cacheName + "::";
        } else {
            cacheKeyPrefix = CacheKeyPrefix.simple();
        }
    }


    @Bean
    RedisCacheManager redisCacheManager() {
        try {
            RedisCacheConfiguration redisCacheConfiguration = forJsonConfig();

            return RedisCacheManager.RedisCacheManagerBuilder
                    .fromConnectionFactory(redisConnectionFactory)
                    .cacheDefaults(redisCacheConfiguration)
		    .withInitialCacheConfigurations(comConfigurationMap())
                    .build();
        } catch(Exception e) {
            log.error("redisCacheManager create Exception", e);
        }

        return null;
    }

    private RedisCacheConfiguration forJsonConfig() {
        ObjectMapper objectMapperForRedisCache = objectMapper.copy();
        objectMapperForRedisCache.setSerializationInclusion(Include.NON_NULL);
        objectMapperForRedisCache.enable(JsonGenerator.Feature.IGNORE_UNKNOWN);
        objectMapperForRedisCache.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        objectMapperForRedisCache.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);

        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapperForRedisCache);

        return RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith  (RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer))
                .computePrefixWith(cacheKeyPrefix)
                ;
    }

    private Map<String, RedisCacheConfiguration> comConfigurationMap() {
        Map<String, RedisCacheConfiguration> map = new TreeMap<>();

	map.put(CKEY, forJsonConfig().entryTtl(Duration.ofSeconds(60)));

        return map;
    }

    @Override
    public CacheErrorHandler errorHandler() {
	return new CacheExceptionHandler();
    }

}

CacheExceptionHandler 는 CacheErrorHandler를 구현하는데, redis에 cache가 조회되지 않는 경우에도 정상적으로 서비스가 동작할 수 있도록 로깅만 처리하도록 한다.

//CacheExceptionHandler.java
import org.springframework.cache.Cache;
import org.springframework.cache.interceptor.CacheErrorHandler;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CacheExceptionHandler implements CacheErrorHandler {

    @Override
    public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
        log.error("{}", exception.getMessage());
        log.error("{}", exception.toString());
    }

    @Override
    public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
        log.error("{}", exception.getMessage());
        log.error("{}", exception.toString());
    }

    @Override
    public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
        log.error("{}", exception.getMessage());
        log.error("{}", exception.toString());
    }

    @Override
    public void handleCacheClearError(RuntimeException exception, Cache cache) {
        log.error("{}", exception.getMessage());
        log.error("{}", exception.toString());
    }

}

 

이제 테스트를 위한 api와 서비스를 작성한다.

//RedisTestController.java -- api 추가
	@GetMapping("/get-cacheable-val/{key}")
    public ResponseEntity<String> getCacheableVal(@PathVariable(name = "key") String key) {

	String val = cacheService.getCacheableVal(key);
	log.debug("cached val is == {}", val);

	return ResponseEntity.ok(val);
    }
    
//CacheServiceImpl.java -- CacheService interface는 생략
import java.util.Date;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.dev.common.config.RedisCacheConfig;
import com.dev.demo.service.CacheService;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class CacheServiceImpl implements CacheService {

    @SuppressWarnings("deprecation")
    @Cacheable(cacheNames = RedisCacheConfig.CKEY, key = "#key", unless = "#result==null")
    @Override
    public String getCacheableVal(String key) {
	// TODO DB 조회 또는 API 호출
	Date dt = new Date();
	int sec = dt.getSeconds();
	log.debug("{}", sec);
	for (int i = 0; i < 100; i++) {
	    if (i == sec) { //호출 시점의 second와 일치하는 값을 출력하고 리턴
		log.debug("i== {}", i);
		return String.valueOf(i);
	    }
	}
	return null;
    }

}

 

테스트를 위해 swagger를 통해 2회 호출해보자.

최초 호출시 51초에 51이라는 값이 정상적으로 리턴되었다. 

51초에 리턴된 값은 51 로 예상과 같다
redisInsight 에도 51

redis insight를 통해서도 redis에 com:CKEY:test1 이라는 키 값에 51이 설정되어있다.

 

다시 호출해보자.

41초에 호출하였는데 51이라는 값이 동일하게 응답되고

몇번을 하더라도 동일한 결과가 리턴된다.

 

콘솔 로그에서도 CacheServiceImpl에서 로깅한 내용은 1회 호출에서만 찍히고,

2회차부터는 Controller에서 response를 주기직전에 logging 한 내용만 찍히는 것을 확인할 수 있다.

 

왜 서비스 콜이 안되는지 헤매는 상황을 방지하려면 RedisCacheConfig에서 entryTtl 설정을 적절하게 잘 설정할 필요가 있다. 

 

Git: cache/redis-cacheable at dev · FullMooney/cache (github.com)

728x90

'개발 > java' 카테고리의 다른 글

springboot resttemplate config 와 restClient 생성  (1) 2024.01.19
springboot rabbitmq config와 DLQ 예제  (0) 2024.01.18
springboot redis client 만들기  (1) 2024.01.08
Logbook 으로 access log 남기기  (2) 2024.01.04
mybatis interceptor 암복호화 처리  (1) 2023.12.13
'개발/java' 카테고리의 다른 글
  • springboot resttemplate config 와 restClient 생성
  • springboot rabbitmq config와 DLQ 예제
  • springboot redis client 만들기
  • Logbook 으로 access log 남기기
yunapapa
yunapapa
working on the cloud
    250x250
  • yunapapa
    supermoon
    yunapapa
  • 전체
    오늘
    어제
    • 분류 전체보기 (94)
      • 개발 (20)
        • java (17)
        • web (2)
        • MSX (1)
        • Go (0)
      • CloudNative (50)
        • App Definition & Developeme.. (17)
        • Orchestration & Management (4)
        • Runtime (3)
        • Provisioning (7)
        • Observability & Analysis (14)
        • event review (5)
      • AWS (7)
      • 환경관련 (17)
      • 취미생활 (0)
        • 맛집 (0)
        • 게임 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • CNCF Past Events
    • Kubernetes Korea Group
  • 공지사항

  • 인기 글

  • 태그

    kubernetes
    오블완
    gitlab
    Pinpoint
    istio
    devops
    티스토리챌린지
    APM
    dop-c02
    Java
    k8s
    springboot
    AWS
    helm
    OpenShift
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
yunapapa
springboot + redis @Cacheable 사용
상단으로

티스토리툴바