- Published on
- •👁️
백엔드 캐싱 II - Spring Cache 고급
- Authors

- Name
- River
상세 설명
Caffeine 캐시란?
등장 배경
- Guava Cache의 한계
- 대규모 데이터 처리 시 성능 이슈
- 간단 LRU 기반으로 정교한 캐시 정책 부족 (LRU - 최근에 안 쓴 것부터 제거)
- 고성능 동시성 처리 한계
- Guava Cache의 교훈을 바탕으로 Caffeine 개발 (10배 이상 빠르다)
- Guava Cache의 한계
Caffeine 특징
Google Guava 캐시의 차세대 버전
JVM 메모리 기반 로컬 캐시
Java 8+ 람다와 스트림 최적화
cache.get(key, k -> expensiveComputation(k));- 즉, 캐시 처리 시 Java 8+ 기능을 활용하여 최적화 가능
W-TinyLFU 알고리즘으로 효율적으로 캐시 제거
- W-TinyLFU (Window Tiny Least Frequently Used)
- 사용 빈도 + 최근성 + 메모리 효율성을 모두 고려
- 즉, 진짜 필요한 데이터는 오래 유지
- W-TinyLFU (Window Tiny Least Frequently Used)
CompletableFuture 지원
CompletableFuture<Product> future = cache.get(productId, id -> CompletableFuture.supplyAsync(() -> loadProductFromDB(id)) );- 비동기 캐시 로딩 지원
- 즉, 무거운 작업을 비동기로 처리할 수 있다.
로컬 캐시의 장단점
- 장점
- 네트워크 I/O 없어서 매우 빠름 (나노초 단위)
- 단점
- 서버별 독립적이라 데이터 불일치 가능
- 장점
CaffeineCacheManager 기본 구성
의존성 추가
dependencies { implementation("com.github.ben-manes.caffeine:caffeine") implementation("org.springframework.boot:spring-boot-starter-cache") }기본 CaffeineCacheManager 설정
@Configuration public class CacheConfig { @Bean public CacheManager caffeineCacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder() .maximumSize(1000) // 최대 1000개 항목 .expireAfterWrite(Duration.ofMinutes(10))); // TTL (10분 후 만료) return manager; } }- 단, Spring Main Class에
@EnableCaching설정 필요
- 단, Spring Main Class에
사용 예시
@Service public class ProductService { @Cacheable(value = "products") public Product findById(Long id) { return productRepository.findById(id); } @CacheEvict(value = "products", key = "#id") public void updateProduct(Long id, Product product) { productRepository.save(product); } }
- 여러 캐시 매니저를 구분해서 사용할 때만 cacheManager 지정
- CacheManager를 지정하지 않으면 Spring은 기본 캐시 매니저를 사용
- 등록된 캐시 매니저가 1개라면 그것이 기본 캐시 매니저가 된다.
- 단, 여러 개의 캐시 매니저를 등록하고 지정을 생략하면 ⇒ 에러 발생
- @Primary 등과 같은 추가 설정이 필요하다.
TTL 전략 (expireAfterWrite vs expireAfterAccess)
expireAfterWrite (쓰기 기준 만료)
@Bean public CacheManager writeBasedCacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder() .expireAfterWrite(Duration.ofMinutes(10)) // 저장된 지 10분 후 만료 .maximumSize(1000)); return manager; }특징
- 데이터가 저장된 시점부터 시간 계산
- 아무리 자주 조회해도 10분 후 무조건 만료
- 데이터 일관성이 중요한 경우 사용
사용 예시
@Cacheable(value = "productPrices") public BigDecimal getProductPrice(Long productId) { return productRepository.findPriceById(productId); }- 상품 가격처럼 정확성이 중요한 데이터
expireAfterAccess (접근 기준 만료)
@Bean public CacheManager accessBasedCacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(30)) // 마지막 접근 후 30분간 유지 .maximumSize(1000)); return manager; }특징
- 마지막 조회 시점부터 시간 계산
- 자주 사용되는 데이터는 계속 유지
- 성능과 메모리 효율성 우선인 경우 사용
사용 예시
@Cacheable(value = "userProfiles") public UserProfile getUserProfile(Long userId) { return userRepository.findProfile(userId); }- 사용자 권한 정보, 최근 조회 상품 목록
두 전략 비교
구분 expireAfterWrite expireAfterAccess 기준 저장 시점 마지막 접근 시점 데이터 일관성 높음 보통 메모리 효율성 예측 가능 높음 적용 사례 상품 정보, 가격 사용자 프로필이나 권한 정보
Caffeine 캐시 TTL 설정 패턴
용도별 캐시 설정
@Configuration public class CacheConfig { // 자주 변하는 데이터 - 짧은 TTL @Bean("shortLivedCache") public CacheManager shortLivedCacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder() .maximumSize(5000) .expireAfterWrite(Duration.ofMinutes(1)) .recordStats()); return manager; } // 안정적인 데이터 - 긴 TTL @Bean("longLivedCache") public CacheManager longLivedCacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(Duration.ofHours(1)) .recordStats()); return manager; } }사용 예시
@Service public class ProductService { // 재고 정보 - 자주 변함 @Cacheable(value = "stock", cacheManager = "shortLivedCache") public Integer getProductStock(Long productId) { return productRepository.getStock(productId); } // 상품 기본 정보 - 안정적 @Cacheable(value = "productInfo", cacheManager = "longLivedCache") public Product getProductInfo(Long productId) { return productRepository.findById(productId); } }
maximumSize와 메모리 관리 전략
항목 수 기반 제한 (기본)
@Bean public CacheManager sizeLimitedCacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder() .maximumSize(5000) // 최대 5000개 항목 .expireAfterWrite(Duration.ofMinutes(15))); return manager; }- 최대 5000개 항목만 저장하도록 제한한다.
메모리 크기 기반 제한
@Bean public CacheManager weightBasedCacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder() .maximumWeight(50_000_000) // 50MB .weigher((String key, Object value) -> { // 간단한 객체 크기 추정 if (value instanceof String) { return ((String) value).length() * 2; // char당 2바이트 } return 1000; // 기본값 1KB }) .expireAfterWrite(Duration.ofMinutes(15))); return manager; }- 자주 쓰이지 않는다
- 메모리 크기를 50 MB로 제한한다.
maximumWeight()로 최대 메모리 크기를 제한한다.- 다만, Caffeine은 객체 크기를 몰라서
weigher()로 개발자가 크기 계산로직을 제공해야 한다.
- 다만, Caffeine은 객체 크기를 몰라서
캐시별 다른 크기 설정
@Configuration public class CacheConfig { @Bean public CacheManager caffeineCacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); // 기본 설정 manager.setCaffeine(Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(Duration.ofMinutes(10))); // 개별 설정 manager.registerCustomCache("userProfiles", Caffeine.newBuilder() .maximumSize(10000) .expireAfterAccess(Duration.ofHours(1)) .build()); manager.registerCustomCache("heavyReports", Caffeine.newBuilder() .maximumSize(50) .expireAfterWrite(Duration.ofMinutes(30)) .build()); return manager; } }registerCustomCache()활용하여 개별 캐시를 설정한다.- 무거운 보고서 객체라면 개수 제한을 작게
- 가벼운 프로필 같은 경우 개수 제한을 크게
통계 수집과 기본 모니터링 ⭐
통계 수집 활성화
@Bean public CacheManager monitoringCacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(Duration.ofMinutes(10)) .recordStats() .removalListener((key, value, cause) -> { log.debug("캐시 제거 - Key: {}, Cause: {}", key, cause); })); return manager; }- 통계 수집 활성 :
recordStats()
- 통계 수집 활성 :
캐시 통계 확인
@Component @RequiredArgsConstructor public class CacheStatsService { private final CacheManager cacheManager; public void printCacheStats(String cacheName) { Cache cache = cacheManager.getCache(cacheName); if (cache instanceof CaffeineCache) { com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache = ((CaffeineCache) cache).getNativeCache(); CacheStats stats = nativeCache.stats(); log.info("=== {} 캐시 통계 ===", cacheName); log.info("Hit Count: {}", stats.hitCount()); log.info("Miss Count: {}", stats.missCount()); log.info("Hit Rate: {:.2f}%", stats.hitRate() * 100); log.info("Eviction Count: {}", stats.evictionCount()); } } }- CacheManager 객체를 활용하여 캐시 정보를 획득
Cache- Spring의 Cache 인터페이스
getNativeCache()- Caffeine 네이티브 객체 가져오기
- Caffine 네이티브 객체의 통계와 같은 기능을 사용하기 위해서
사용 예시
@Service @Slf4j public class ProductService { @Autowired private CacheStatsService cacheStatsService; @Cacheable(value = "products") public Product findById(Long id) { log.debug("DB에서 상품 조회 - ID: {}", id); return productRepository.findById(id); } @Scheduled(fixedRate = 300000) // 5분마다 public void logCacheStats() { cacheStatsService.printCacheStats("products"); } }
메모리 사용량 최적화 팁
적절한 maximumSize 설정
@Bean public CacheManager optimizedCacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder() .maximumSize(2000) // 적당한 크기 .expireAfterWrite(Duration.ofMinutes(15)) // 적당한 TTL .recordStats()); return manager; }메모리 사용량 추정
- 작은 객체 (ID, 이름)
- 100-500 바이트 ⇒
maximumSize: 10,000-50,000
- 100-500 바이트 ⇒
- 중간 객체 (Entity)
- 1-5 KB ⇒
maximumSize: 1,000-5,000
- 1-5 KB ⇒
- 큰 객체 (리스트, 집계)
- 10-100 KB ⇒
maximumSize: 100-1,000
- 10-100 KB ⇒
- 작은 객체 (ID, 이름)
메모리 사용량을 추정하고 그에 맞는 적절한
maximumSize를 설정하는 것이 좋다.
불필요한 캐시 제거
- 자주 조회되고 변경이 적은 데이터만 캐시 ✅
- 자주 변하는 데이터는 캐시하지 않는다. ❌
상세 설명
Redis 캐시의 특징과 장점
Redis vs Caffeine 비교
구분 Redis (글로벌캐시) Caffeine (로컬캐시) 위치 별도 서버 JVM 내부 공유 서버 간 공유 가능 서버별 독립적 속도 빠름 (밀리초) 매우 빠름 (나노초) 영속성 재시작 후에도 유지 가능 재시작 시 소실 메모리 Redis 서버 메모리 JVM 힙 메모리 확장성 클러스터 확장 가능 단일 서버 제한 Redis 캐시 사용 시나리오
- 분산 환경의 데이터 공유
- 여러 서버가 동일한 데이터를 일관성 있게 공유해야 할 때 사용
- 세션 클러스터링
- 로드 밸런서 환경에서 여러 서버가 사용자의 세션 정보를 공유하여 로그인 상태 등을 유지
- 대용량 데이터 캐싱
- 로컬 메모리에 부담을 주지 않으면서 대규모 데이터를 캐시에 저장해 빠르게 조회
- 데이터 영속성
- 서버가 재시작되어도 데이터를 디스크에 저장하여 유실 없이 보존
- 분산 환경의 데이터 공유
RedisCacheManager 기본 설정
의존성 추가
dependencies { implementation("org.springframework.boot:spring-boot-starter-cache") implementation("org.springframework.boot:spring-boot-starter-data-redis") }application.yml 설정
spring: redis: host: localhost port: 6379 password: ${REDIS_PASSWORD:} timeout: 2000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0- Redis의 SSL/TLS 설정 및 고가용성(Sentinel, Cluster) 설정은 이후 내용에서 다룬다.
- Spring Boot는
spring-boot-starter-data-redis의존성이 추가되면, 별도의 Java 설정 없이도 application.yml의 정보를 바탕으로 RedisConnectionFactory와 RedisCacheManager를 자동으로 구성한다. - 아래의
@Bean을 통한 설정 방식은 세부 정책을 커스터마이징하고 싶을 때 사용하는 방법
Bean을 통한 Redis 연결 설정
@Configuration public class RedisCacheConfig { @Bean public RedisConnectionFactory redisConnectionFactory() { LettuceConnectionFactory factory = new LettuceConnectionFactory( new RedisStandaloneConfiguration("localhost", 6379) ); return factory; } @Bean public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) // 기본 30분 TTL .disableCachingNullValues(); // null 값 캐싱 비활성화 return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .build(); } }단, Spring Main Class에
@EnableCaching설정 필요- RedisConnectionFactory
- Redis 서버와 어떻게 연결할지 설정
- Lettuce
- Redis Java 클라이언트 라이브러리 (비동기, 논블로킹)
- 다른 방식 :
Jedis(동기)
- RedisStandaloneConfiguration
- 단일 Redis 서버 (클러스터 X)
- 다른 방식 :
RedisClusterConfiguration,RedisSentinelConfiguration
- CacheManager
- 캐싱 정책 설정
RedisConnectionFactory를 사용하여 생성한다.- RedisTemplate도 마찬가지로
RedisConnectionFactory를 통해 생성하지만, RedisTemplate는 수동 캐싱을 구현할 때 사용한다.
직렬화 전략
직렬화가 필요한 이유
- Redis는 문자열이나 바이너리 데이터만 저장할 수 있는데, Java 객체는 더 복잡한 메모리 구조라 직렬화가 필요하다.
- 즉, 저장이 가능한 형태로 바꾸는 것이다.
- 이를 다시 역직렬화를 통해 Java 객체로 변환할 수도 있다.
- null 값 캐싱 방지 :
disableCachingNullValues() - 직렬화 설정 방법
.serializeKeysWith()- Key는 항상 문자열로
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())SerializationPair: 직렬화 + 역직렬화fromSerializer(): 직렬화 방식 설정StringRedisSerializer: 문자열 ↔ 바이트 변환 (Key 직렬화에 보통 사용)- "product:123" ⇒ UTF-8 바이트
.serializeValuesWith()- Value는 직렬화 방식에 따라
- 바이너리
serializeValuesWith(JdkSerializationRedisSerializer)
- JSON
.serializeValuesWith(jsonSerializer)
JDK 직렬화 (기본값)
@Bean public CacheManager jdkSerializationCacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .disableCachingNullValues() .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new JdkSerializationRedisSerializer())); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .build(); }직렬화 설정 추가
new JdkSerializationRedisSerializer()- Java 객체 ↔ 바이너리 변환
- Value 직렬화에 사용
- Java 객체 ⇒
\xac\xed\x00\x05sr...
특징
Spring Cache의 기본 직렬화 방식
바이너리 데이터로 저장되어 용량 효율적
- Java 객체 그래프 완전 보존
(객체 그래프는 Java 객체 포함 관계를 말한다)
단점
- Redis CLI에서 내용 확인 불가 (바이너리)
- 클래스 변경 시 호환성 문제
- 저장했던 클래스와 필드가 달라지거나 하면 읽을 수 없다 (
InvalidClassException발생)
- 저장했던 클래스와 필드가 달라지거나 하면 읽을 수 없다 (
- 다른 언어와 호환성 없음
- 바이너리 형식을 몰라서 Python에서 Redis의 데이터를 읽을 수 없다.
JSON 직렬화 (권장)
@Bean public CacheManager jsonSerializationCacheManager(RedisConnectionFactory connectionFactory) { // Jackson 설정 ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping( BasicPolymorphicTypeValidator.builder() .allowIfSubType(Object.class) .build(), ObjectMapper.DefaultTyping.NON_FINAL ); // JSON 직렬화 설정 GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .disableCachingNullValues() .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(jsonSerializer)) .entryTtl(Duration.ofMinutes(30)); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .build(); }직렬화 설정 추가
ObjectMapper 설정
- setVisibility( )
PropertyAccessor.ALL- 모든 접근 방식Visibility.ANY- 접근 제한자 상관없이 모두 허용 (private 필드도 포함하기 위해)
- activateDefaultTyping( )
- 타입 정보 저장 (JSON에 클래스 정보 포함시켜서 정확한 복원 가능하도록)
BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build()LaissezFaireSubTypeValidator.instance- 이렇게 설정할 수도 있다. 다만 이것은 어떤 클래스든 역직렬화 허용
- 즉, 보안 위험 존재
ObjectMapper.DefaultTyping.NON_FINAL- final이 아닌 클래스에만 타입 정보 추가
- 즉, String, Integer 같은 final 클래스는 타입이 명확하니까 제외
- setVisibility( )
- new GenericJackson2JsonRedisSerializer(objectMapper)
- Java 객체 ↔ JSON 변환
- Value 직렬화에 사용
- Java 객체 ⇒
{"id":1,"name":"iPhone"}
장점
- Redis CLI에서 내용 확인 가능
- 다른 언어/시스템과 호환성 좋음
- 스키마 변경에 상대적으로 유연하다
단점
- 바이너리보다 용량 큼
- 복잡한 객체 그래프 처리 시 성능 이슈 가능
캐시별 개별 TTL 설정
캐시별 다른 TTL 전략
@Bean public CacheManager multiTtlCacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())) .entryTtl(Duration.ofMinutes(30)); // 기본 30분 // 개별 캐시 설정 Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>(); cacheConfigurations.put("stock", defaultConfig .entryTtl(Duration.ofMinutes(1))); // 자주 변하는 데이터 cacheConfigurations.put("userProfiles", defaultConfig .entryTtl(Duration.ofMinutes(30))); // 사용자 프로필 - 중간 TTL cacheConfigurations.put("products", defaultConfig .entryTtl(Duration.ofHours(2))); // 상품 정보 - 긴 TTL cacheConfigurations.put("categories", defaultConfig .entryTtl(Duration.ofHours(24))); // 정적 데이터 - 매우 긴 TTL return RedisCacheManager.builder(connectionFactory) .cacheDefaults(defaultConfig) .withInitialCacheConfigurations(cacheConfigurations) .build(); }
Redis 연결 최적화
Connection Pool 설정
@Bean public RedisConnectionFactory optimizedRedisConnectionFactory() { GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>(); poolConfig.setMaxTotal(20); // 최대 연결 수 poolConfig.setMaxIdle(10); // 최대 유휴 연결 수 poolConfig.setMinIdle(5); // 최소 유휴 연결 수 poolConfig.setTestOnBorrow(true); // 대여 시 연결 테스트 LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder() .commandTimeout(Duration.ofMilliseconds(1000)) .poolConfig(poolConfig) .build(); LettuceConnectionFactory factory = new LettuceConnectionFactory( new RedisStandaloneConfiguration("localhost", 6379), clientConfig ); return factory; }RedisConnectionFactory를 생성할 때 세부 설정을 할 수 있다.
new LettuceConnectionFactory()생성 시 커스텀 config 설정을 같이 넘긴다.
Lettuce 커넥션 풀 설정 (커스텀 config 설정)
GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();setMaxTotal(20)- 최대 연결 수 (커넥션)
setMaxIdle(10)- 최대 유휴 연결 수
- 사용하지 않는 연결이 설정된 값을 넘으면 초과분 제거 (메모리 절약)
setMinIdle(5)- 최소 유휴 연결 수
- 빠른 응답을 위한 워밍업으로 미리 준비하는 연결 수
setTestOnBorrow(true)- 연결을 빌려줄 때 연결 상태를 검증하는 것
- 끊어진 연결 사용을 방지하기 위함으로, ping 테스트
추가 튜닝 요소
poolConfig.setMaxWaitMillis(3000); // 연결 대기 시간 poolConfig.setTimeBetweenEvictionRunsMillis(30000); // 유휴 연결 정리 주기 poolConfig.setTestWhileIdle(true); // 유휴 중 연결 테스트 poolConfig.setBlockWhenExhausted(true); // 풀이 고갈될 때 대기 여부 poolConfig.setJmxEnabled(true); // JMX 모니터링 활성화
앞서 말했듯
application.yml설정만으로도 구성이 가능은 가능하다spring: redis: lettuce: pool: max-active: 20 # MaxTotal과 동일 max-idle: 10 # MaxIdle과 동일 min-idle: 5 # MinIdle과 동일 test-on-borrow: true- 세밀한 제어 필요 시 Bean을 생성하여 설정하면 된다.
실무 Redis 연결 설정 패턴
@Configuration public class RedisConfig { @Value("${spring.redis.host}") private String redisHost; @Value("${spring.redis.port}") private int redisPort; @Value("${spring.redis.password:}") private String redisPassword; @Bean public RedisConnectionFactory redisConnectionFactory() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); config.setHostName(redisHost); config.setPort(redisPort); if (!redisPassword.isEmpty()) { config.setPassword(redisPassword); } LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() .commandTimeout(Duration.ofSeconds(2)) .shutdownTimeout(Duration.ofMillis(100)) .build(); return new LettuceConnectionFactory(config, clientConfig); } }RedisStandaloneConfiguration 설정
- 마찬가지로 application.yml 설정만으로 Spring Boot가 자동으로
RedisConnectionFactory를 생성한다.
- 마찬가지로 application.yml 설정만으로 Spring Boot가 자동으로
LettuceClientConfiguration 설정
commandTimeout()- Redis 명령 실행 시 최대 대기 시간
shutdownTimeout()- 애플리케이션 종료 시 Redis 연결을 정리하는데 걸리는 최대 시간
실무 사용 예시
멀티 레벨 캐시 전략
@Service public class ProductService { // L1: Caffeine (초고속) - 자주 조회되는 핫데이터 @Cacheable(value = "hotProducts", cacheManager = "caffeineCacheManager") public Product getHotProduct(Long productId) { return getProductFromRedis(productId); } // L2: Redis (분산 공유) - 일반적인 상품 데이터 @Cacheable(value = "products", cacheManager = "redisCacheManager") public Product getProductFromRedis(Long productId) { return productRepository.findById(productId); } // 캐시 무효화는 양쪽 모두 @Caching(evict = { @CacheEvict(value = "hotProducts", key = "#productId", cacheManager = "caffeineCacheManager"), @CacheEvict(value = "products", key = "#productId", cacheManager = "redisCacheManager") }) public void updateProduct(Long productId, Product product) { productRepository.save(product); } }- 멀티 레벨 캐시로 세밀하게 캐싱 전략을 커스텀할 수 있다.
세션 공유
@Service public class UserSessionService { @Cacheable(value = "userSessions", cacheManager = "redisCacheManager") public UserSession getUserSession(String sessionId) { // return sessionRepository.findBySessionId(sessionId); } @CacheEvict(value = "userSessions", key = "#sessionId", cacheManager = "redisCacheManager") public UserSession updateUserSession(String sessionId, UserSession session) { return sessionRepository.save(session); } }@Cacheable활용하여 Redis에서 세션 정보 조회 (서버 간 공유)@CacheEvict활용하여 캐시 무효화
Redis 캐시 모니터링 기초
Redis 상태 확인
@Component public class RedisHealthIndicator implements HealthIndicator { private final RedisTemplate<String, Object> redisTemplate; @Override public Health health() { try { String ping = redisTemplate.getConnectionFactory() .getConnection().ping(); return Health.up() .withDetail("ping", ping) .withDetail("totalKeys", redisTemplate.keys("*").size()) .build(); } catch (Exception e) { return Health.down() .withException(e) .build(); } } }HealthIndicator 클래스
Spring Boot Actuator의 헬스체크 인터페이스
public interface HealthIndicator { Health health(); }클래스명에 따라
/actuator/health/{이름}엔드포인트 자동 생성RedisHealthIndicator⇒/actuator/health/redis자동 생성DatabaseHealthIndicator⇒/actuator/health/database자동 생성
application.yml 설정 필요
management: endpoint: health: show-details: always # when-authorized (인증 사용자), never (기본값)- 없으면 단순히 UP/DOWN만 보여준다.
- 환경별로 다르게 설정하는 것이 일반적이다. (보안 이유)
- 보안상 상세 정보를 숨기기 위해
when-authorized를 쓰거나, 관리용 포트로 분리할 수 있다.
캐시 메트릭 수집
@Component public class RedisCacheMetrics { private final MeterRegistry meterRegistry; private final RedisTemplate<String, Object> redisTemplate; @Scheduled(fixedRate = 60000) public void collectCacheMetrics() { try { Properties info = redisTemplate.getConnectionFactory() .getConnection().info(); // Micrometer로 메트릭 등록 Gauge.builder("redis.memory.used") .register(meterRegistry, () -> parseMemory(info.getProperty("used_memory"))); Gauge.builder("redis.clients.connected") .register(meterRegistry, () -> Integer.parseInt(info.getProperty("connected_clients"))); } catch (Exception e) { log.warn("Redis 메트릭 수집 실패", e); } } }/actuator/metrics- 모든 메트릭 목록/actuator/metrics/redis.memory.used- 특정 메트릭 값/actuator/prometheus- Prometheus 형식으로 모든 메트릭MeterRegistry 클래스
- Micrometer의 메트릭 저장소
- 다양한 메트릭(카운터, 게이지, 타이머)을 수집/저장
Gauge- 실시간 수치 측정 타입 (메모리 사용량 등)Counter- 증가만 하는 값 (요청 수, 에러 수)Timer- 실행 시간 측정
- Prometheus, Grafana, CloudWatch 등으로 내보내기
application.yml 설정 필요
management: endpoints: web: exposure: include: metrics, prometheus metrics: export: prometheus: enabled: true
상세 설명
SpEL 기본 문법
셀렉션 연산자 (Selection Operators)
*// 예시: #numbers = [3, 7, 2, 9, 1]* #numbers.^[#this > 5] *// 결과: 7 (첫 번째)* #numbers.$[#this > 5] *// 결과: 9 (마지막)* #numbers.?[#this > 5] *// 결과: [7, 9] (모든 요소)*^[조건]- 조건을 만족하는 첫 번째 요소 반환$[조건]- 조건을 만족하는 마지막 요소 반환?[조건]- 조건을 만족하는 모든 요소를 새 컬렉션으로 반환
프로젝션 연산자 (Projection Operator) ⭐
#users.![#this.name] *// 모든 사용자의 이름 추출* #numbers.![#this * 2] *// 모든 숫자에 2를 곱함*![표현식]- 각 요소에 표현식을 적용하여 새 컬렉션 생성
조건식 타입
- Boolean 리터럴
#users.^[true]- 항상 참 (첫 번째 요소)
- 비교 연산
#users.^[#this.age >= 18]- 조건 만족하는 요소
- 메서드 호출
#users.^[#this.isActive()]- 메서드 결과로 판단
- 복합 조건
#users.^[#this.age > 20 && #this.isActive()]- AND/OR 연산
- null 체크
#users.^[#this != null]- null이 아닌 요소
- Boolean 리터럴
기본 접근 연산자
[인덱스]- 배열/리스트의 특정 위치 요소 접근.length/.size()- 배열 길이 / 컬렉션 크기.isEmpty()- 컬렉션이 비어있는지 확인
SpEL 메서드 호출과 동적 키 생성
객체 메서드 직접 호출
객체의 메서드를 SpEL에서 직접 호출하여 동적 키 생성
사용자 권한에 따른 동적 키
@Service public class DynamicCacheService { @Cacheable(value = "userContent", key = "#user.getRole().toUpperCase() + ':' + #contentType + ':' + #user.hasPermission('PREMIUM')") public Content getUserContent(User user, String contentType) { return contentService.getContentByUserRole(user, contentType); } }#user.getRole().toUpperCase()- 메서드 체이닝으로 대문자 변환
#user.hasPermission('PREMIUM')- 권한 체크 메서드 직접 호출
- 예시 -
"MANAGER:video:false","ADMIN:music:true"
복잡한 메서드 체이닝
@Service public class DynamicCacheService { @Cacheable(value = "regionData", key = "#request.getLocation().getCountry().getCode() + ':' + #request.getLanguage().toLowerCase()") public RegionalData getRegionalData(LocalizedRequest request) { return regionService.getData(request.getLocation(), request.getLanguage()); } }- 복잡한 메서드 체이닝이여도 SpEL에서 사용 가능하다.
- 예시 -
"KR:en","US:es"
조건부 메서드 호출
사용자 타입에 따른 다른 키 생성 로직
@Service public class ConditionalCacheService { @Cacheable(value = "dynamicUserData", key = "#user.isGuest() ? 'guest:' + #user.getSessionId() : 'member:' + #user.getId() + ':' + #user.getLastLoginDate().format(T(java.time.format.DateTimeFormatter).ofPattern('yyyyMM'))") public UserData getUserData(User user) { return userService.getData(user); } }- 삼항 연사자를 활용하여 조건에 따라 다른 키를 생성
- 회원인 경우만 마지막 로그인 날짜를 넣기
- 예시 -
"guest:abc123xyz","member:12345:202412"
시간대별 다른 캐시 정책
@Service public class ConditionalCacheService { @Cacheable(value = "timeBasedData", key = "T(java.time.LocalTime).now().getHour() < 9 ? 'night:' + #dataType : 'day:' + #dataType + ':' + #priority.toString()") public BusinessData getTimeBasedData(String dataType, Priority priority) { return businessService.getData(dataType, priority); } }- 정적 메서드(ex.
LocalTime.now())를 활용하여 조건에 따라 다른 키를 생성 - 예시 -
"night:sales","day:inventory:MEDIUM"
- 정적 메서드(ex.
SpEL에서 컬렉션과 배열 처리
컬렉션과 배열의 요소 접근
리스트 첫 번째 요소 활용
@Cacheable(value = "categoryProducts", key = "#categories.isEmpty() ? 'default' : #categories[0] + ':size:' + #categories.size()") public List<Product> getProductsByCategories(List<String> categories) { return productService.findByCategories(categories); }- 배열이 비어있는 경우 조건 처리
#categories[0]- 리스트 첫 번째 요소 접근 (무조건 첫번째 요소)#categories.^[true]- 조건 만족하는 첫번째 요소- 조건이
true이므로 첫번째 요소를 바로 가져오므로 첫번째 요소 선택과 같다. - 다만,
#categories[0]보다 안전한 접근 방식이다.
- 조건이
- 예시 -
"default","electronics:size:3","books:size:1"
배열 마지막 요소 활용
@Cacheable(value = "pathData", key = "#pathSegments.length > 0 ? #pathSegments[#pathSegments.length-1] : 'root'") public PathData getDataByPath(String[] pathSegments) { return pathService.getData(pathSegments); }#pathSegments[#pathSegments.length-1]- 배열 마지막 요소#categories.$[true]- 조건 만족하는 마지막 요소- 예시 -
"root","profile","file.txt"- 웹사이트 경로 : /admin/users/profile
- 파일 시스템 경로 : /home/user/documents/file.txt
컬렉션 필터링 ⭐
@Cacheable(value = "activeUsers", key = "'active:' + #users.![#this.isActive()].size() + ':total:' + #users.size()") public UserSummary getUserSummary(List<User> users) { return userService.summarize(users); } @Cacheable(value = "activeUsers", key = "'active:' + #users.?[#this.isActive()].size() + ':total:' + #users.size()")#users.![#this.isActive()]- 컬렉션 프로젝션으로 필터링- 각 요소에 표현식을 적용 후 모든 결과 요소를 결과 컬렉션 반환
- 여기서 프로젝션은 단순히 모든 요소를 메서드 호출 처리하는 것 뿐이다.
- 예시 -
"active:5:total:5"
#users.?[#this.isActive()]- 컬렉션 셀렉션으로 필터링- 각 요소에 표현식을 적용 후 조건에 맞는 요소만을 선택하여 필터링된 결과 컬렉션 반환
isActive()결과가 크기에 영향을 미친다.- 예시 -
"active:3:total:5"
컬렉션 연산자 활용
컬렉션 조건 검사
@Cacheable(value = "validatedData", key = "#ids.?[#this > 0].size() + ':validated:' + #validateAll") public ValidationResult validateIds(List<Long> ids, boolean validateAll) { return validationService.validate(ids, validateAll); }boolean validateAll로 엄격히 검증하는 경우와 아닌 경우 나눠서 키 생성#ids.?[#this > 0]- 조건에 맞는 요소 필터링#this는 현재 반복되고 있는 컬렉션의 각 요소이다.- 즉,
List<Long> ids = Arrays.asList(1L, -2L, 3L, 0L, 5L, -7L);에서 조건에 맞는 양수 ID만 필터링한다.
- 예시 -
"3:validated:true","4:validated:false","0:validated:true"
컬렉션 변환 및 조합
@Cacheable(value = "transformedData", key = "'sorted:' + #tags.![#this.toLowerCase()].^[true] + ':' + #tags.size()") public ProcessedData processWithTags(List<String> tags) { return processor.process(tags); }#tags.![#this.toLowerCase()]- 모든 요소에 변환 적용List<String> tags = Arrays.asList("JAVA", "Spring", "REDIS", "mysql");일 때,["java", "spring", "redis", "mysql"]를 반환한다.
#tags.^[true]- 첫 번째 요소 선택- 예시 -
"sorted:java:4","sorted:null:0"
SpEL의 정규표현식과 문자열 고급 처리
정규표현식 활용
이메일 도메인 추출
@Cacheable(value = "domainStats", key = "#email.matches('.+@(.+)') ? #email.replaceAll('.+@(.+)', '$1') : 'invalid'") public DomainStatistics getDomainStats(String email) { return statsService.getByEmailDomain(email); }.+@(.+)정규표현식.- 줄바꿈 문자 제외 모든 단일 문자+- 바로 앞의 패턴이 1번 이상 반복된다는 의미- 즉,
.+는 ".", "123", "!@#” 등 줄바꿈 제외 모든 문자가 1번이상 반복되는 것 (와)는 이렇게 묶으면 별도의 하나의 그룹으로 캡처$1은 첫 번째 캡처 그룹((...))에 의해 캡처된 문자열을 참조
String 클래스 메서드의
matches(),replaceAll()활용- 정규표현식을 통해 이메일 형식이 맞는지 파악 후
- 형식이 맞다면, 도메인만 가져오기
- 형식이 틀리다면, invalid 설정
- 정규표현식을 통해 이메일 형식이 맞는지 파악 후
예시 -
"example.com","sub.domain.co.kr","invalid"
전화번호 형식 정규화
@Cacheable(value = "phoneData", key = "#phone.replaceAll('[^0-9]', '') + ':' + #region") public PhoneInfo getPhoneInfo(String phone, String region) { return phoneService.getInfo(phone, region); }- 전화번호 문자열에서 숫자가 아닌 모든 문자를 제거한다.
- 예시 -
"01012345678:KR","1234567890:US"
URL 경로 파싱
@Cacheable(value = "urlCache", key = "#url.matches('https?://[^/]+/(.+)') ? #url.replaceAll('https?://[^/]+/(.+)', '$1').split('/')[0] : 'home'") public PageData getPageData(String url) { return pageService.getData(url); }- 정규표현식
https?- ?는 수량자 표현으로 바로 앞의s가 있거나 없을 수도 있다는 의미[^/]+- 문자 세트를 정의, 이것은 문자들 중 하나에 매칭된다.^는 부정의 의미로,[^/]는/를 제외한 모든 문자에 매칭한다는 의미+로 인해 이런한 매칭이 1번이상 반복된다는 의미
- 도메인 이후의 경로만을 추출하고 이 경로를
/를 구분자로 분리 후 제일 첫 경로만을 사용 - 예시 -
"products","home"
- 정규표현식
문자열 고급 연산
문자열 슬라이싱과 변환
@Cacheable(value = "textProcessing", key = "#text.length() > 50 ? #text.substring(0, 50).replaceAll('\\s+', '_') + '_truncated' : #text.replaceAll('\\s+', '_')") public ProcessedText processText(String text) { return textProcessor.process(text); }\\s+- 모든 공백 문자(\s)가 한 번 이상(+) 반복- 파라미터로 받은 문자열의 길이가 50 초과라면, 50까지만 추출하고
_truncated을 붙이고, 아니라면 그냥 사용 - 모든 공백 문자를
_로 전환한다. - 예시 -
"This_is_a_very_long_text_that_exceeds_fifty_c_truncated"
해시 기반 키 생성
@Cacheable(value = "hashedKeys", key = "T(java.lang.String).valueOf(T(java.util.Objects).hash(#param1, #param2, #param3)).substring(0, 8)") public ComplexData getComplexData(String param1, String param2, String param3) { return complexService.getData(param1, param2, param3); }- 여러 개의 입력 파라미터(
param1,param2,param3)를 사용하여 고유한 해시 값 기반의 캐시 키를 생성 - 해시 기반 문자열의 앞 8자리만 사용
- 예시 -
"3817f2a4","9d2b5c7e"
- 여러 개의 입력 파라미터(
SpEL의 환경 변수 및 시스템 속성 정보 활용
환경 변수와 시스템 속성
환경별 캐시 분리
@Cacheable(value = "envSpecificData", key = "T(System).getProperty('spring.profiles.active', 'default') + ':' + #dataType") public EnvironmentData getEnvironmentData(String dataType) { return environmentService.getData(dataType); }- 자바 런타임 시스템의 시스템 프로퍼티에 따라 캐시된 데이터를 분리한다.
- Spring의 경우, spring.profiles.active와 같은 프로필 정보가 자동으로 JVM 시스템 프로퍼티로 설정되거나 환경 변수로 주입된다.
T(System).getProperty('spring.profiles.active', 'default')- System 클래스는 자바 런타임 시스템에 대한 표준 입출력, 시스템 프로퍼티, 환경 변수 등 다양한 시스템 관련 유틸리티 메서드를 제공
- 이를 통해 설정된 profile 정보를 읽어온다. 단, 없다면 default를 사용
- 예시 -
"prod:users","default:boards"
- 자바 런타임 시스템의 시스템 프로퍼티에 따라 캐시된 데이터를 분리한다.
JVM 정보 기반 키
@Cacheable(value = "jvmData", key = "'jvm:' + T(System).getProperty('java.version') + ':heap:' + (T(Runtime).getRuntime().maxMemory() / 1024 / 1024) + 'MB'") public JvmStatistics getJvmStats() { return jvmService.getStatistics(); }- JVM의 자바 버전과 최대 힙 메모리를 읽어온다.
- JVM의 시스템 속성과 현재 JVM 런타임 환경에 대한 정보를 직접 가져오는 자바 표준 라이브러리 함수 활용
- Runtime 클래스는 자바 애플리케이션의 런타임 환경과 상호작용할 수 있는 클래스
- 예시 -
"jvm:17.0.3:heap:4096MB"
- JVM의 자바 버전과 최대 힙 메모리를 읽어온다.
서버 정보 포함
@Cacheable(value = "serverData", key = "T(java.net.InetAddress).getLocalHost().getHostName() + ':' + #requestType") public ServerResponse getServerData(String requestType) { return serverService.process(requestType); }- 분산 환경에서 각 서버의 캐시를 고유하게 유지하거나 서버 인스턴스 문제를 디버깅할 때 유용하다.
- java.net.InetAddress 클래스
- IP 주소를 나타내는 Java 클래스
- IP 주소 표현 (IPv4, IPv6)
- 호스트 이름(예:
www.google.com) 조회 - 로컬 호스트 정보
- 예시 -
"my-app-server-01:config"
동적 시간 기반 키
비즈니스 시간 기반 (9-18시는 1분, 그 외는 10분 캐시)
@Cacheable(value = "businessHourData", key = "#dataId + ':' + (T(java.time.LocalTime).now().getHour() >= 9 && T(java.time.LocalTime).now().getHour() <= 18 ? T(System).currentTimeMillis() / 60000 : T(System).currentTimeMillis() / 600000)") public BusinessData getBusinessHourData(Long dataId) { return businessService.getData(dataId); }T(System).currentTimeMillis() / 60000- 비즈니스 시간 내에 실행된다.
- 분 단위 마다 캐시를 갱신하겠다는 의미 (분이 달라져야 새로운 키 저장)
T(System).currentTimeMillis() / 600000- 비즈니스 시간이 아닌 경우 실행된다.
- 10분 단위 마다 캐시를 갱신하겠다는 의미
- 예시 -
"123:2598765432"
월별/주별 데이터와 영구 데이터 동적 분기
@Cacheable(value = "periodicData", key = "#isPermanent ? 'permanent:' + #userId : 'monthly:' + T(java.time.YearMonth).now().toString() + ':' + #userId") public UserData getPeriodicData(Long userId, boolean isPermanent) { return userDataService.getData(userId, isPermanent); }boolean isPermanent을 통해 영구 데이터 여부를 판단 후 동적 키 생성- 예시 -
"permanent:456","monthly:2025-07:456"
커스텀 유틸리티 클래스 활용
복잡한 캐시 키 생성 로직을 @Cacheable 애노테이션으로 직접 작성하는 대신, 별도의 유틸리티 메서드로 분리하여 관리하는 방식
캐시 키 생성 전용 유틸리티 생성
public class CacheKeyUtil { public static String generateTimeSlotKey(String baseKey) { LocalTime now = LocalTime.now(); String timeSlot; if (now.isBefore(LocalTime.of(6, 0))) { timeSlot = "dawn"; } else if (now.isBefore(LocalTime.of(12, 0))) { timeSlot = "morning"; } else if (now.isBefore(LocalTime.of(18, 0))) { timeSlot = "afternoon"; } else { timeSlot = "evening"; } return baseKey + ":" + timeSlot; } public static String hashLongParams(Object... params) { return String.valueOf(Objects.hash(params)).substring(0, 8); } }baseKey를 인자로 받아 기본 키에 시간대 정보를 추가하는 방식- 복잡하거나 긴 파라미터를 해시 기반 키로 변환해야 할 때, 재사용
캐시 키 생성 전용 유틸리티 활용
@Service public class UtilityBasedCacheService { // 시간대별 키 생성 @Cacheable(value = "timeSlotData", key = "T(com.example.util.CacheKeyUtil).generateTimeSlotKey(#baseKey)") public TimeSlotData getTimeSlotData(String baseKey) { return timeSlotService.getData(baseKey); } // 긴 파라미터 해시 처리 @Cacheable(value = "complexParams", key = "T(com.example.util.CacheKeyUtil).hashLongParams(#param1, #param2, #param3, #param4)") public ComplexResult processComplexParams(ComplexObject param1, AnotherObject param2, String param3, List<String> param4) { return complexProcessor.process(param1, param2, param3, param4); } }- 이러한 유틸리티 클래스는 SpEL에서 정적 메서드 호출로 사용된다.
커스텀 KeyGenerator 활용
커스텀 keyGenerator 사용하기
@Cacheable(value = "products", keyGenerator = "customKeyGenerator") public Product complexMethod(Object... params) { ... }KeyGenerator 구현체 만들기
@Component("customKeyGenerator") public class CustomKeyGenerator implements KeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder keyBuilder = new StringBuilder(); keyBuilder.append(target.getClass().getSimpleName()); keyBuilder.append(":"); keyBuilder.append(method.getName()); for (Object param : params) { keyBuilder.append(":"); keyBuilder.append(param.toString()); } return keyBuilder.toString(); } }- 이름은 원하는 이름으로 작성
- 복잡한 키가 필요할 때만 keyGenerator 사용
컨텍스트 인식 KeyGenerator
@Component("contextAwareKeyGenerator") public class ContextAwareKeyGenerator implements KeyGenerator { @Autowired private SecurityContextHolder securityContext; @Override public Object generate(Object target, Method method, Object... params) { StringBuilder keyBuilder = new StringBuilder(); // 로그인 사용자 캐시 분리 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated()) { keyBuilder.append("user:").append(auth.getName()).append(":"); } else { keyBuilder.append("anonymous:"); } // 권한이 필요한 메서드 분리 if (method.isAnnotationPresent(AdminOnly.class)) { keyBuilder.append("admin:"); } // 파라미터 타입별 분리 for (Object param : params) { if (param instanceof Pageable) { Pageable page = (Pageable) param; keyBuilder.append("page:").append(page.getPageNumber()) .append(":size:").append(page.getPageSize()).append(":"); } else if (param instanceof SearchCriteria) { SearchCriteria criteria = (SearchCriteria) param; keyBuilder.append("search:").append(criteria.hashCode()).append(":"); } else { keyBuilder.append(param != null ? param.toString() : "null").append(":"); } } return keyBuilder.toString(); } }- 캐시 키를 단순히 메서드 인자로만 구성하는 것이 아니라, 특정 컨텍스트를 반영하여 세밀하게 제어할 수 있다.
- 로그인 사용자 캐시 분리
- 로그인 사용자 정보가 있다면, 다른 사용자와 분리하여 캐시 키를 설정한다.
- 예시 :
user:alice:,anonymous:123:
- 권한이 필요한 메서드 분리
- 캐시를 활용하는 메서드가 특정 권한을 가져야 한다면, 별도의 캐시 키를 설정한다.
- 예시 :
user:bob:admin:reportlist:,user:charlie:
- 파라미터 타입별 분리
- 파라미터 중에서 페이징이나, 커스텀 검색 클래스 관련한 요소라면, 별도 설정한다.
- 예시
user:dave:admin:page:0:size:20:user:emma:search:-987654321:user:frank:books:anonymous:null:
성능 최적화 KeyGenerator
@Component("performanceKeyGenerator") public class PerformanceOptimizedKeyGenerator implements KeyGenerator { private static final int MAX_KEY_LENGTH = 100; private final MessageDigest digest; public PerformanceOptimizedKeyGenerator() { try { this.digest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("MD5 not available", e); } } @Override public Object generate(Object target, Method method, Object... params) { StringBuilder rawKey = new StringBuilder(); rawKey.append(method.getName()).append(":"); for (Object param : params) { if (param != null) { rawKey.append(param.toString()).append(":"); } } String key = rawKey.toString(); // 키가 너무 길면 해시 처리 if (key.length() > MAX_KEY_LENGTH) { synchronized (digest) { digest.reset(); byte[] hash = digest.digest(key.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(hash); } } return key; } }메서드 이름을 포함하고, 값이 null이 아닌 모든 파라미터를 포함한다.
다만, 캐시 키 생성 시 성능과 효율성에 초점을 둔다.
- 복잡한 파라미터가 있는 경우 처럼 캐시 키가 너무 길어지면
- 메모리 사용량 증가
- 키를 저장/조회하는 데 필요한 네트워크 전송량 및 처리 시간도 길어질 수 있다.
MAX_KEY_LENGTH를 설정하여 지나치게 긴 경우 해시 처리
- 복잡한 파라미터가 있는 경우 처럼 캐시 키가 너무 길어지면
- 해시 처리
- 데이터 노출 최소화 (민감 정보 차단)
- 키 비교 및 인덱싱에서 더 효율적
커스텀 CacheResolver 활용
커스텀 CacheResolver 구현체 생성
@Component("smartCacheResolver") public class SmartCacheResolver implements CacheResolver { @Override public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) { String methodName = context.getMethod().getName(); Object[] args = context.getArgs(); if (methodName.startsWith("getRealtime")) { return Arrays.asList(fastCache.getCache("realtime")); // 빠른 캐시 } if (args.length > 0 && args[0] != null && args[0].toString().startsWith("temp_")) { return Arrays.asList(fastCache.getCache("temporary")); // 임시 캐시 } return Arrays.asList(persistentCache.getCache("default")); // Redis 캐시 } }- 메서드 이름이나 파라미터로 판단
- 런타임에 메서드 정보를 분석해서 적절한 캐시를 자동 선택
커스텀 CacheResolver 사용
@Cacheable(cacheResolver = "smartCacheResolver") public Data getRealtimeData(String key) { return dataService.getData(key); } @Cacheable(cacheResolver = "smartCacheResolver") public Data getTemporaryData(String temp_key) { return dataService.getData(temp_key); } @Cacheable(cacheResolver = "smartCacheResolver") public Data getNormalData(String key) { return dataService.getData(key); }- 메서드 이름, 파라미터 등을 보고 자동으로 캐시를 동적 선택
cacheManager보다 더 똑똑한 동적 선택- 언제 사용?
- 매우 복잡한 캐시 선택 로직이 필요할 때만 실무에서 드물게 사용
상세 설명
d
현재 작성 중입니다