본문 바로가기
JAVA

Spring Cache를 파헤쳐 보자!(WebMvc, WebFlux)

by 열정적인 이찬형 2023. 12. 13.

상황

 

  • 여러 로직에서 간단한 정보를 조회가 반복되어 DB I/O 지속적으로 생기는 상황
    • 자주 변하지도 않는 정보를 계속 I/O 생기도록 유지하는 것이 맞는가??
  • 프로젝트 시연 중, 갑자기 조회하는 로직에서 시간을 무지막지하게 잡아먹는 상황
    • 원인 파악하기에는 시간이 부족하고….
    • Redis와 같은 외부 캐시 서버를 사용할 상황이 아닐 때….

Redis는 너무 Over-Spec이야 어떻게 해야 하지???

► Spring Cache!!!

 

원인을 파악할 시간이 없어… 일단 조회 관련 급한 불은 꺼야 하는데

► Spring Cache!!!

 


Caching?

오랜시간이 걸리는 작업의 결과를 저장해서 시간과 비용을 필요로 회피하는 기법

캐시가 있을 때

1 → 2 → 3 → 6

 

캐시가 없을 때

1 → 2 → 4 → 5 → 6

 

TTL(Time To Live)

캐쉬를 생성할 때 캐쉬의 만료기간을 정해두는 것이다.

 

지정된 만료일이 지나면 캐쉬를 삭제하고 다시 캐쉬를 생성하는 기법이다.

 


Spring Cache을 도입하기 전 고려사항

 

1. Docker Swarm, K8s 등 컨테이너 오케스트레이션을 통해 Scale Out이 발생할 수 상황인가??

 

Scale Out이 발생하면, Replication이 진행되면서, 동일한 역할을 하는 서버가 여러 개가 생기지만, 각 서버는 다른 Pod에 존재하기 때문에 독립적인 인스턴스를 가져서 Spring Cache가 가지고 있는 데이터들에 대한 일관성이 깨질 수 있습니다.

  1. Client가 Pod1에 저장된 Cache 데이터를 조회
  2. Pod1 Cache한 데이터가 임의의 이유로 데이터가 변경
  3. Pod2에는 변경된 데이터가 Caching 되어 있음
  4. Pod2에서 Cache 데이터를 조회했지만, Pod1과는 다른 데이터가 응답

위와 같은 상황일 때 데이터 일관성에 대한 문제가 발생할 수 있습니다.

 

Redis을 Cache Server로 사용한다면?

※ Pod끼리 통신할 때에는 위 그림처럼 직접적으로 통신하지 않습니다!!! 위 그림은 이해하기 쉽게 하기 위해서 직접 통신하는 것처럼 표현한 것입니다.

 

Spring Server들이 동일한 Cache 서버를 가리키고 Caching한 데이터를 조회하기 때문에 데이터 일관성을 만족하게 됩니다.

 


 

💡 Redis도 Scale Out이 되면 데이터 일관성이 깨질 수도 있는 것 아니냐???

 

Redis Cluster에 대해서 알아보는 것을 추천드립니다.

 

Scale with Redis Cluster

Horizontal scaling with Redis Cluster

redis.io

 


 

2. Caching을 진행할 데이터의 양이 많은가??

 

Spring Cache는 Spring Server가 동작하는 JVM의 메모리를 사용하기 때문에 많은 양의 데이터에 대해서 Caching을 진행하면 성능적인 영향을 끼칠 수 있습니다.

OutOfMemoryError와 같은 문제 발생

GC의 발생 빈도가 증가로 애플리케이션의 일시적인 지연이 발생

외부 캐시 서버을 이용하면 메모리를 관리를 외부 캐시 서버가 하기 때문에 Spring Server에 대해서 영향을 끼치지 않습니다!!

 

또한, Spring Server가 모종의 이유로 Down되더라도 외부 캐시 서버는 영향을 받지 않기 때문에 Caching된 데이터들을 유지할 수 있습니다.

 

→ Spring Cache는 Spring Server와 같이 다 날라감 😭

 

하지만, Redis와의 I/O가 발생하는 단점도 존재합니다.

 


💡 Redis!! Redis!! Redis!! 그러는데 넌 정체가 무엇이냐!!

먼저, 공식 문서를 살펴봐주세요!! 🙏🙏🙏🙏🙏🙏🙏🙏

 

추후에 정리해서 올리겠습니다.

 

Introduction to Redis

Learn about the Redis open source project

redis.io

 


고려사항에 만족하는 상황에서 Spring Cache을 사용해서 발생하는 Side Effect에 대해서는


Spring Cache Manager

 

 

  • ConcurrentMapCacheManager
    • JRE에서 제공하는 ConcurrentHashMap을 캐시 저장소로 사용할 수 있는 구현체다.
    • 캐시 정보를 Map타입(Key, Value)으로 메모리에 저장해두기 때문에 빠르고 별다른 설정이 필요 없다는 장점이 있다.
    • 실제 서비스에서 사용하기엔 기능이 빈약하다.
    • TTL도 지원하지 않기 때문에, 캐시 데이터를 저장/조회/확인 기능만 지원합니다.
  • SimpleCacheManager
    • Spring에서 제공하는 가장 기본적인 캐시 매니저 중 하나입니다.
    • 기본적으로 제공하는 캐시가 없다.
    • 사용할 캐시를 직접 등록하여 사용하기 위한 캐시 매니저 구현체다.
    • AbstractCacheManager 구현체로 사용하기 위해 상속받고 있습니다.

  • EhCacheCacheManager
    • Java에서 유명한 캐시 프레임워크 중 하나인 EhCache를 지원하는 캐시 매니저 구현체다.
    • TTL(Time-To-Live), TTI(Time-To-Idle)을 지원합니다.
    • 로컬 힙에 메모리 저장 최대 개수 및 캐싱 데이터 영구성을 선언할 수 있습니다.
    • 영구성을 선언하면 TTL이 지나도 삭제되지 않고, 수동으로만 캐싱 데이터를 삭제할 수 있습니다.
  • CaffeineCacheManager
    • Java 8로 Guava 캐시를 재작성한 Caffeine 캐시 저장소를 사용할 수 있는 구현체다.
    • EhCache와 함께 인기 있는 매니저인데, 이보다 좋은 성능을 갖는다고 한다.
    • TTL(Time-To-Live), TTI(Time-To-Idle) 지원
    • 최대 크기 설정으로 메모리를 효율적으로 사용 가능(캐시 크기 초과시 LRU(Least Recently Use, 페이지 교체 알고리즘) 수행)
    • Stream, 통계 및 모니터링, 비동기 로딩, 낮은 지연시간, 다양한 설정옵션 등
  • JCacheCacheManager
    • JSR-107 표준을 따르는 JCache 캐시 저장소를 사용할 수 있는 구현체다.
    • Java에서 캐싱을 표준화하는 데 목적이 있습니다.
    • 표준화된 API/어노테이션/캐시 이벤트 지원 등 캐싱에 대한 표준을 제공하고 있습니다.
  • RedisCacheManager
    • Redis를 캐시 저장소로 사용할 수 있는 구현체다.
    • 캐시 그룹화, TTL(Time-To-Live), (Key, Value) 직렬화, Spring 어노테이션 통합 등 지원
  • CompositeCacheManager
    • 한 개 이상의 캐시 매니저를 사용할 수 있는 혼합 캐시 매니저다.
    • 각 캐시 매니저의 장점을 활용하면서 더욱 다양한 캐시 전략을 구현할 수 있습니다.
    • 각 캐시 매니저에는 우선순위를 부여할 수 있습니다.
    • 캐시 조회 시에는 우선순위에 따라 순차적으로 캐시 매니저를 조회하고, 첫 번째로 캐시를 찾으면 해당 결과를 반환합니다.

CaffeineCacheManager 사용해보기

 

Local Cache을 진행해보기 위해서 RedisCache는 사용하지 않고, CaffeineCacheManager을 사용해 볼 것입니다.

 

확장성 및 성능상 뛰어나다는 평가를 받고 있기 때문에 선정하였습니다.

 

의존성 추가

Gradle

implementation 'com.github.ben-manes.caffeine:caffeine:3.1.6'
implementation 'org.springframework.boot:spring-boot-starter-cache'

 

Maven

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>
</dependencies>

 

Spring Boot Starter Cache : Spring에서 Cache 관련 기능들을 지원합니다.

 

Caffeine Cache : Caffeine Cache관련 기능을 지원합니다.

 

Config

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@EnableCaching
@Configuration
public class CacheConfig {

    @Bean(name = "KeyCacheManager")
    public CacheManager keyCacheManager(){
        //Cache 정의된 정보 가져오기(Enum)
        CacheType cacheType = CacheType.KEY_CACHE;
        //CaffeineCacheManager 생성
        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(cacheType.getCacheName());
        //Caffeine 설정
        caffeineCacheManager.setCaffeine(
                Caffeine.newBuilder()
                        .maximumSize(cacheType.getMaximumSize())  //Caffeine 최대 개수
                        .expireAfterWrite(Duration.ofSeconds(cacheType.getExpireAfterWriteOfSeconds()))  //TTL 설정
                        .recordStats()); //통계 처리

        return caffeineCacheManager;
    }
}

 

@EnableCaching 이용해서 사용중인 Spring에 Caching을 사용한다는 것을 정의합니다.

 

의존성 주입을 통해서 사용하기 위해서 Bean을 만들기 위해서 @Configuration을 통해서 ComponentScan

에서 처리되도록 하였습니다.

 

CacheManager을 1개만 사용할 것이라면, application 파일에 환경변수로 설정해주면 자동으로 만들어집니다.

spring:
        cache:
        caffeine:
        spec: maximumSize=500,expireAfterWrite=5s

 

Enum(CacheManager 정의 정보)

import lombok.Getter;

@Getter
public enum CacheType {
    KEY_CACHE("KeyCache", 1000, 10);

    CacheType(String cacheName, int maximumSize, int expireAfterWriteOfSeconds) {
        this.cacheName = cacheName;
        this.maximumSize = maximumSize;
        this.expireAfterWriteOfSeconds = expireAfterWriteOfSeconds;
    }
	
    /**
     *  cacheName : 캐시 이름
     *  maximumSize : 캐시 최대 사이즈
     *  expireAfterWriteOfSeconds : 캐시 만료 시간(TTL)
     */

    private String cacheName;
    private int maximumSize;
    private int expireAfterWriteOfSeconds;
}

 

Caching

WebMVC

@Service
public class TestServiceImpl implements TestService {


    @Cacheable(cacheNames = "KeyCache", key = "#parameter", cacheManager = "KeyCacheManager")
    @Override
    public Long test(String parameter) {
		System.out.println("캐싱 데이터 존재하지 않음!!");
        return System.currentTimeMillis();
    }
	
    @Cacheput(cacheNames = "KeyCache", key = "#parameter", cacheManager = "KeyCacheManager")
    @Override
    public Long putTest(String parameter) {
		System.out.println("캐싱 데이터를 저장하지만 조회하지는 않습니다.");
        return System.currentTimeMillis();
    }
	
    @CacheEvict(cacheNames = "KeyCache", key = "#parameter", cacheManager = "KeyCacheManager")
    @Override
    public void clearTest(String parameter) {
        System.out.println(parameter + "의 해당하는 캐싱 데이터를 삭제합니다!");
    }

}

 

WebFlux

@Service
public class TestServiceImpl implements TestService{

    @Cacheable(value = "KeyCache", key = "#parameter", cacheManager = "KeyCacheManager")
    @Override
    public Mono<Long> cacheable(String parameter) {
        System.out.println("캐싱 데이터 존재하지 않음!!");
        return Mono.defer(() -> {
            long time = System.currentTimeMillis();
            System.out.println("삐빅... 비즈니스 로직 실행 중... 시간은 " + time + "입니다.");
            return Mono.just(time);
        }).cache(Duration.ofSeconds(CacheType.NCP_KEY_CACHE.getExpireAfterWriteOfSeconds()));
    }
    
    @Cacheable(value = "KeyCache", key = "#parameter", cacheManager = "KeyCacheManager")
    @Override
    public Mono<Long> cacheableOfFuture(String parameter) {
        System.out.println("캐싱 데이터 존재하지 않음!!");
        CompletableFuture<Long> future = CompletableFuture.supplyAsync(() ->{
            long time = System.currentTimeMillis();
            System.out.println("삐빅... 비즈니스 로직 실행 중... 시간은 " + time + "입니다.");
            return time;
        });
        return Mono.fromFuture(future).cache(Duration.ofSeconds(CacheType.NCP_KEY_CACHE.getExpireAfterWriteOfSeconds()));
    }

    @CachePut(value = "KeyCache", key = "#parameter", cacheManager = "KeyCacheManager")
    @Override
    public Mono<Long> putCache(String parameter) {
        System.out.println("캐싱 데이터를 저장하지만 조회하지는 않습니다.");
        return Mono.defer(() -> {
            long time = System.currentTimeMillis();
            System.out.println("삐빅... 비즈니스 로직 실행 중... 시간은 " + time + "입니다.");
            return Mono.just(time);
        }).cache(Duration.ofSeconds(CacheType.NCP_KEY_CACHE.getExpireAfterWriteOfSeconds()));
    }
    
    @CacheEvict(value = "KeyCache", key = "#parameter", cacheManager = "KeyCacheManager")
    @Override
    public Mono<Void> evictCache(String parameter) {
        System.out.println(parameter + "의 해당하는 캐싱 데이터를 삭제합니다!");
        return Mono.empty();
    }
}

 

(중요) WebFlux환경에서는 Spring Cache을 적용할 때 유의할 점이 존재합니다.

 

WebFlux에서는 위처럼 Mono 객체가 캐싱이 진행되며, Mono/Flux는 구독자가 subscribe()를 하기 이전까지는 실제로 실행되지 않고 조립 단계만 수행한다.

 

➢ 캐싱된 Mono객체는 캐시하고자 하는 값이 아닌 Mono 체인을 조립한 객체입니다.

➢ 즉, 조립한 Mono을 다시 구독하는 것으로 캐시의 효과를 하나도 볼 수 없습니다.

 

예시.

	
    @Cacheable(value = "KeyCache", key = "#parameter", cacheManager = "KeyCacheManager")
    @Override
    public Mono<Long> cacheable(String parameter) {
        System.out.println("캐싱 데이터 존재하지 않음!!");
        return Mono.defer(() -> {
            long time = System.currentTimeMillis();
            System.out.println("삐빅... 비즈니스 로직 실행 중... 시간은 " + time + "입니다.");
            return Mono.just(time);
        });
    }

 

반복 호출 결과

 

 

Mono의 조립단계을 Caching 한 것이기 때문에 조립 단계에 저장된 로직을 다시 실행해서 Caching의 효과를 볼 수 없습니다.

 

Mono.cache()을 이용해서 해당 결과에 대한 값을 Caching할 수 있습니다.

    @Cacheable(value = "KeyCache", key = "#parameter", cacheManager = "KeyCacheManager")
    @Override
    public Mono<Long> cacheable(String parameter) {
        System.out.println("캐싱 데이터 존재하지 않음!!");
        return Mono.defer(() -> {
            long time = System.currentTimeMillis();
            System.out.println("삐빅... 비즈니스 로직 실행 중... 시간은 " + time + "입니다.");
            return Mono.just(time)
        }).cache(Duration.ofSeconds(CacheType.KEY_CACHE.getExpireAfterWriteOfSeconds()));
    }

 

반복 호출 결과

 

 

위와 같이 비즈니스 로직이 실행되지 않고 해당 결과가 Caching 된 것을 확인할 수 있습니다.

 

또한 Java8부터 지원한 비동기을 지원하는 CompletableFutrer()과 Mono.fromFuture()을 이용해서 비동기로 로직을 처리해서 사용해도 됩니다.

 

@Cacheable(value = "KeyCache", key = "#parameter", cacheManager = "KeyCacheManager")
@Override
public Mono<Long> cacheable(String parameter) {
    System.out.println("캐싱 데이터 존재하지 않음!!");
    CompletableFuture<Long> future = CompletableFuture.supplyAsync(() ->{
        long time = System.currentTimeMillis();
        System.out.println("삐빅... 비즈s니스 로직 실행 중... 시간은 " + time + "입니다.");
        return time;
    });
    return Mono.fromFuture(future).cache(Duration.ofSeconds(CacheType.KEY_CACHE.getExpireAfterWriteOfSeconds()));
}

 

반복 호출 결과

 

Reactor + CompletableFuture을 같이 활용함으로써 Caching이 진행되는 것을 확인하실 수 있습니다.

 

 

Reator환경에서 Spring Cache을 사용할 때 발생하는 문제점은 존재합니다.

 

Mono.cache()subscribe가 시작되었을 때를 기준으로 TTL이 계산됩니다.

 

CacheManagerCaching이 되었을 때를 기준으로 TTL이 계산됩니다.

 

→ 즉, CacheManger의 TTL과 Mono.cache()의 TTL의 차이가 발생할 수 있다는 것입니다.

 

그래서 WebFlux에서 Spring Cache을 사용할 때에는 거의 변하지 않는 값을 Caching하는 것을 추천드립니다.

 


WebFlux Spring Cache 관련 Research을 하다보면 Reactor Addons을 사용하여 Custom Annotation이나 로직을 구성하라는 내용이 있습니다.

 

Reactor 3.6.0 버전부터는 CacheMono(), CacheFlux()가 deprecated가 되었기 때문에 사용하지 않는 것을 추천드립니다.

 

 

3.6.0 deprecated removal: CacheFlux and CacheMono · Issue #263 · reactor/reactor-addons

See #237

github.com

 

deprecated가 된 이유와 이슈들은 아래 링크에서 확인하실 수 있습니다.

 

Deprecating cache utilities (removal in 3.6.0) · Issue #237 · reactor/reactor-addons

Motivation CacheMono and CacheFlux helpers were an attempt at providing a sane solution to adapt various cache / means of caching into a reactive facade. There is very little bandwidth to maintain ...

github.com

 

간단하게 정리해서 말씀드리면

 

Reactor Addons에 CacheMono(), CacheFlux()을 사용해서 Caching을 진행한 한 프로젝트들에서 성능적으로 큰 이점이 없었으며, 오히려 Side Effect을 발생시키는 현상이 일어났기 때문에 deprecated 되었습니다.

 

또한, Caching을 사용하실 때, 비동기 기반일 경우 Mono.toFuture나 Mono.fromFuture을 활용하는 것을 추천드린다.


Annotation

 

@Cacheable

 

캐시 저장소에 캐시 데이터를 저장하거나 조회하는 기능을 사용할 수 있다.

캐시 저장소에 데이터가 있으면 조회하고, 없으면 로직을 실행한 뒤 캐시 저장소에 저장합니다.

 

 

Property Description
value : String[] cacheName의 alias

Default : {}
cacheNames : String[] 캐시 이름들(설정 Method 리턴값이 저장되는 공간)

Default : {}
key : String 동일한 cache name을 사용하지만 구분될 필요가 있을 때 사용되는 키 값

Default : ""
keyGenerator : String Key를 동적으로 생성할 때 사용하는 KeyGenerator의 구현체를 지정하는 데 사용

Default : ""
cacheManager : String 사용 할 CacheManager 지정

Default : ""
cacheResolver : String CacheManager를 동적으로 선택할 때 사용하는 KeyGenerator의 구현체를 지정하는 데 사용

Default : ""
condition : String or, and 등 조건식 및 논리연산으로 True일 경우에만 캐싱 적용

Default : ""
unless : String or, and 등 조건식 및 논리연산으로 True일 경우에 캐싱 적용되지 않음

Default : ""
sync : boolean 캐시 구현체가 Thread safe 하지 않는 경우, 캐시에 동기화를 걸 수 있는 속성

Default : false

 

Spring Inner Code

 
더보기
  /**
	 * Alias for {@link #cacheNames}.
	 */
	@AliasFor("cacheNames")
	String[] value() default {};

	/**
	 * Names of the caches in which method invocation results are stored.
	 * Names may be used to determine the target cache (or caches), matching
	 * the qualifier value or bean name of a specific bean definition.
	 * @since 4.2
	 * @see #value
	 * @see CacheConfig#cacheNames
	 */
	@AliasFor("value")
	String[] cacheNames() default {};

	/**
	 * Spring Expression Language (SpEL) expression for computing the key dynamically.
	 * Default is {@code ""}, meaning all method parameters are considered as a key,
	 * unless a custom {@link #keyGenerator} has been configured.
	 * The SpEL expression evaluates against a dedicated context that provides the
	 * following meta-data:
	 *  • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for
	 *     references to the {@link java.lang.reflect.Method method}, target object, and
	 *     affected cache(s) respectively.
	 *  • Shortcuts for the method name ({@code #root.methodName}) and target class
	 *    ({@code #root.targetClass}) are also available.
	 *  • Method arguments can be accessed by index. For instance the second argument
	 *    can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
	 *    can also be accessed by name if that information is available.
	 */
	String key() default "";

	/**
	 * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator}
	 * to use.
	 * Mutually exclusive with the {@link #key} attribute.
	 * @see CacheConfig#keyGenerator
	 */
	String keyGenerator() default "";

	/**
	 * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to
	 * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none
	 * is set already.
	 * Mutually exclusive with the {@link #cacheResolver}  attribute.
	 * @see org.springframework.cache.interceptor.SimpleCacheResolver
	 * @see CacheConfig#cacheManager
	 */
	String cacheManager() default "";

	/**
	 * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver}
	 * to use.
	 * @see CacheConfig#cacheResolver
	 */
	String cacheResolver() default "";

	/**
	 * Spring Expression Language (SpEL) expression used for making the method
	 * caching conditional. Cache the result if the condition evaluates to
	 * {@code true}.
	 * Default is {@code ""}, meaning the method result is always cached.
	 * The SpEL expression evaluates against a dedicated context that provides the
	 * following meta-data:
	 *  • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for
	 *    references to the {@link java.lang.reflect.Method method}, target object, and
	 *    affected cache(s) respectively.
	 *  • Shortcuts for the method name ({@code #root.methodName}) and target class
	 *    ({@code #root.targetClass}) are also available.
	 *  • Method arguments can be accessed by index. For instance the second argument
	 *    can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
	 *    can also be accessed by name if that information is available.
	 */
	String condition() default "";

	/**
	 * Spring Expression Language (SpEL) expression used to veto method caching.
	 * Veto caching the result if the condition evaluates to {@code true}.
	 * Unlike {@link #condition}, this expression is evaluated after the method
	 * has been called and can therefore refer to the {@code result}.
	 * Default is {@code ""}, meaning that caching is never vetoed.
	 * The SpEL expression evaluates against a dedicated context that provides the
	 * following meta-data:
	 *  • {@code #result} for a reference to the result of the method invocation. For
	 *    supported wrappers such as {@code Optional}, {@code #result} refers to the actual
	 *    object, not the wrapper
	 *  • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for
	 *    references to the {@link java.lang.reflect.Method method}, target object, and
	 *    affected cache(s) respectively.
	 *  • Shortcuts for the method name ({@code #root.methodName}) and target class
	 *    ({@code #root.targetClass}) are also available.
	 *  • Method arguments can be accessed by index. For instance the second argument
	 *    can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
	 *    can also be accessed by name if that information is available.
	 * @since 3.2
	 */
	String unless() default "";

	/**
	 * Synchronize the invocation of the underlying method if several threads are
	 * attempting to load a value for the same key. The synchronization leads to
	 * a couple of limitations:
	 *  • {@link #unless()} is not supported
	 *  • Only one cache may be specified
	 *  • No other cache-related operation can be combined
	 * This is effectively a hint and the actual cache provider that you are
	 * using may not support it in a synchronized fashion. Check your provider
	 * documentation for more details on the actual semantics.
	 * @since 4.3
	 * @see org.springframework.cache.Cache#get(Object, Callable)
	 */
	boolean sync() default false;

 

@CachePut

 

실행 결과를 캐시에 저장합니다. 하지만, 조회 시에 저장된 캐시의 내용을 사용하지는 않고 항상 메소드의 로직을 실행한다.

 

Property Description
value : String[] cacheName의 alias

Default : {}
cacheNames : String[] 캐시 이름들(설정 Method 리턴값이 저장되는 공간)

Default : {}
key : String 동일한 cache name을 사용하지만 구분될 필요가 있을 때 사용되는 키 값

Default : ""
keyGenerator : String Key를 동적으로 생성할 때 사용하는 KeyGenerator의 구현체를 지정하는 데 사용

Default : ""
cacheManager : String 사용 할 CacheManager 지정

Default : ""
cacheResolver : String CacheManager를 동적으로 선택할 때 사용하는 KeyGenerator의 구현체를 지정하는 데 사용

Default : ""
condition : String or, and 등 조건식 및 논리연산으로 True일 경우에만 캐싱 적용

Default : ""
unless : String or, and 등 조건식 및 논리연산으로 True일 경우에 캐싱 적용되지 않음

Default : ""

 

Spring Inner Code

더보기
  /**
	 * Alias for {@link #cacheNames}.
	 */
	@AliasFor("cacheNames")
	String[] value() default {};

	/**
	 * Names of the caches to use for the cache put operation.
	 * Names may be used to determine the target cache (or caches), matching
	 * the qualifier value or bean name of a specific bean definition.
	 * @since 4.2
	 * @see #value
	 * @see CacheConfig#cacheNames
	 */
	@AliasFor("value")
	String[] cacheNames() default {};

	/**
	 * Spring Expression Language (SpEL) expression for computing the key dynamically.
	 * Default is {@code ""}, meaning all method parameters are considered as a key,
	 * unless a custom {@link #keyGenerator} has been set.
	 * The SpEL expression evaluates against a dedicated context that provides the
	 * following meta-data:
	 *  • {@code #result} for a reference to the result of the method invocation. For
	 *    supported wrappers such as {@code Optional}, {@code #result} refers to the actual
	 *    object, not the wrapper
	 *  • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for
	 *    references to the {@link java.lang.reflect.Method method}, target object, and
	 *    affected cache(s) respectively.
	 *  • Shortcuts for the method name ({@code #root.methodName}) and target class
	 *    ({@code #root.targetClass}) are also available.
	 *  • Method arguments can be accessed by index. For instance the second argument
	 *    can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
	 *    can also be accessed by name if that information is available.
	 */
	String key() default "";

	/**
	 * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator}
	 * to use.
	 * Mutually exclusive with the {@link #key} attribute.
	 * @see CacheConfig#keyGenerator
	 */
	String keyGenerator() default "";

	/**
	 * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to
	 * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none
	 * is set already.
	 * Mutually exclusive with the {@link #cacheResolver} attribute.
	 * @see org.springframework.cache.interceptor.SimpleCacheResolver
	 * @see CacheConfig#cacheManager
	 */
	String cacheManager() default "";

	/**
	 * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver}
	 * to use.
	 * @see CacheConfig#cacheResolver
	 */
	String cacheResolver() default "";

	/**
	 * Spring Expression Language (SpEL) expression used for making the cache
	 * put operation conditional. Update the cache if the condition evaluates to
	 * {@code true}.
	 * This expression is evaluated after the method has been called due to the
	 * nature of the put operation and can therefore refer to the {@code result}.
	 * Default is {@code ""}, meaning the method result is always cached.
	 * The SpEL expression evaluates against a dedicated context that provides the
	 * following meta-data:
	 *  • {@code #result} for a reference to the result of the method invocation. For
	 *    supported wrappers such as {@code Optional}, {@code #result} refers to the actual
	 *    object, not the wrapper
	 *  • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for
	 *    references to the {@link java.lang.reflect.Method method}, target object, and
	 *    affected cache(s) respectively.
	 *  • Shortcuts for the method name ({@code #root.methodName}) and target class
	 *    ({@code #root.targetClass}) are also available.
	 *  • Method arguments can be accessed by index. For instance the second argument
	 *    can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
	 *    can also be accessed by name if that information is available.
	 */
	String condition() default "";

	/**
	 * Spring Expression Language (SpEL) expression used to veto the cache put operation.
	 * Veto updating the cache if the condition evaluates to {@code true}.
	 * Default is {@code ""}, meaning that caching is never vetoed.
	 * The SpEL expression evaluates against a dedicated context that provides the
	 * following meta-data:
	 *  • {@code #result} for a reference to the result of the method invocation. For
	 *    supported wrappers such as {@code Optional}, {@code #result} refers to the actual
	 *    object, not the wrapper
	 *  • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for
	 *    references to the {@link java.lang.reflect.Method method}, target object, and
	 *    affected cache(s) respectively.
	 *  • Shortcuts for the method name ({@code #root.methodName}) and target class
	 *    ({@code #root.targetClass}) are also available.
	 *  • Method arguments can be accessed by index. For instance the second argument
	 *    can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
	 *    can also be accessed by name if that information is available.
	 * @since 3.2
	 */
	String unless() default "";

 

@CacheEvict

 

캐시 데이터를 캐시에서 제거하는 목적으로 사용된다.

→ 주로, 원본 데이터를 변경하거나 삭제할 때 사용합니다.

 

Property Description
value : String[] cacheName의 alias

Default : {}
cacheNames : String[] 캐시 이름들(설정 Method 리턴값이 저장되는 공간)

Default : {}
key : String 동일한 cache name을 사용하지만 구분될 필요가 있을 때 사용되는 키 값

Default : ""
keyGenerator : String Key를 동적으로 생성할 때 사용하는 KeyGenerator의 구현체를 지정하는 데 사용

Default : ""
cacheManager : String 사용 할 CacheManager 지정

Default : ""
cacheResolver : String CacheManager를 동적으로 선택할 때 사용하는 KeyGenerator의 구현체를 지정하는 데 사용

Default : ""
condition : String or, and 등 조건식 및 논리연산으로 True일 경우에만 캐싱 적용

Default : ""
allEntries : boolean True로 설정하면 해당 캐시의 모든 엔트리가 제거되어 캐시를 완전히 비웁니다

Default : False
beforeInvocation : boolean True 일 경우 메서드 수행 이전 캐시 리소스 삭제,
False 일 경우 메서드 수행 후 캐시 리소스 삭제.
Default : False

 

Spring Inner Code

더보기
/**
	 * Alias for {@link #cacheNames}.
	 */
	@AliasFor("cacheNames")
	String[] value() default {};

	/**
	 * Names of the caches to use for the cache eviction operation.
	 * Names may be used to determine the target cache (or caches), matching
	 * the qualifier value or bean name of a specific bean definition.
	 * @since 4.2
	 * @see #value
	 * @see CacheConfig#cacheNames
	 */
	@AliasFor("value")
	String[] cacheNames() default {};

	/**
	 * Spring Expression Language (SpEL) expression for computing the key dynamically.
	 * Default is {@code ""}, meaning all method parameters are considered as a key,
	 * unless a custom {@link #keyGenerator} has been set.
	 * The SpEL expression evaluates against a dedicated context that provides the
	 * following meta-data:
	 *  • {@code #result} for a reference to the result of the method invocation, which
	 *    can only be used if {@link #beforeInvocation()} is {@code false}. For supported
	 *    wrappers such as {@code Optional}, {@code #result} refers to the actual object,
	 *    not the wrapper
	 *  • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for
	 *    references to the {@link java.lang.reflect.Method method}, target object, and
	 *    affected cache(s) respectively.
	 *  • Shortcuts for the method name ({@code #root.methodName}) and target class
	 *    ({@code #root.targetClass}) are also available.
	 *  • Method arguments can be accessed by index. For instance the second argument
	 *    can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
	 *    can also be accessed by name if that information is available.
	 */
	String key() default "";

	/**
	 * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator}
	 * to use.
	 * Mutually exclusive with the {@link #key} attribute.
	 * @see CacheConfig#keyGenerator
	 */
	String keyGenerator() default "";

	/**
	 * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to
	 * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none
	 * is set already.
	 * Mutually exclusive with the {@link #cacheResolver} attribute.
	 * @see org.springframework.cache.interceptor.SimpleCacheResolver
	 * @see CacheConfig#cacheManager
	 */
	String cacheManager() default "";

	/**
	 * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver}
	 * to use.
	 * @see CacheConfig#cacheResolver
	 */
	String cacheResolver() default "";

	/**
	 * Spring Expression Language (SpEL) expression used for making the cache
	 * eviction operation conditional. Evict that cache if the condition evaluates
	 * to {@code true}.
	 * Default is {@code ""}, meaning the cache eviction is always performed.
	 * The SpEL expression evaluates against a dedicated context that provides the
	 * following meta-data:
	 *  • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for
	 *    references to the {@link java.lang.reflect.Method method}, target object, and
	 *    affected cache(s) respectively.
	 *  • Shortcuts for the method name ({@code #root.methodName}) and target class
	 *    ({@code #root.targetClass}) are also available.
	 *  • Method arguments can be accessed by index. For instance the second argument
	 *    can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
	 *    can also be accessed by name if that information is available.
	 */
	String condition() default "";

	/**
	 * Whether all the entries inside the cache(s) are removed.
	 * By default, only the value under the associated key is removed.
	 * Note that setting this parameter to {@code true} and specifying a
	 * {@link #key} is not allowed.
	 */
	boolean allEntries() default false;

	/**
	 * Whether the eviction should occur before the method is invoked.
	 * Setting this attribute to {@code true}, causes the eviction to
	 * occur irrespective of the method outcome (i.e., whether it threw an
	 * exception or not).
	 * Defaults to {@code false}, meaning that the cache eviction operation
	 * will occur <em>after</em> the advised method is invoked successfully (i.e.
	 * only if the invocation did not throw an exception).
	 */
	boolean beforeInvocation() default false;

 

keyGenerator, cacheResolver은 동적으로 그에 맞는 key값이나 manager을 선택할 수 있도록 구현체를 구현해서 적용시켜주어야 합니다.

 

예시.

//KeyGenerator 구현체
public static class CustomKeyGenerator extends AbstractKeyGenerator {

        @Override
        protected Object generateKey(Object target, java.lang.reflect.Method method, Object... params) {
            // 동적으로 키를 생성하는 로직을 구현
            // target: 호출 대상 객체, method: 호출된 메서드, params: 메서드의 파라미터
            return "CustomKey-" + params[0] + params[1];
        }
 }
-------------------------------------------------------------
//CacheResolver 구현체
public static class CustomCacheResolver extends AbstractCacheResolver {

        @Override
        protected Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
            // 동적으로 캐시를 선택 또는 생성하는 로직을 구현
            // context를 통해 메서드 실행 정보 등을 활용할 수 있음
            return getCacheManager().getCache("dynamicCache");
        }
}
-------------------------------------------------------------
//Method
@Cacheable(cacheNames = "KeyCache", keyGenerator = CustomKeyGenerator.class, cacheResolver = CustomCacheResolver.class)
@Override
public Long test(String parameter) {
		System.out.println("캐싱 데이터 존재하지 않음!!");
    return System.currentTimeMillis();
}

 

테스트

 

상황1.

  1. Test API 호출(캐싱 진행)
  2. n초 후 Test API 재호출(캐싱 데이터 조회)

상황2.

  1. putTest API 호출(캐싱 진행)
  2. n초 후 putTest API 호출(캐싱 진행)
  3. n초 후 Test API 호출(캐싱 데이터 조회)

상황3.

  1. Test API 호출(캐싱 진행)
  2. n초 후 putTest API 호출(캐싱 진행)
  3. Test API 호출(2번에서 캐싱 데이터 조회)
  4. evictTest API 호출(캐싱 데이터 삭제)
  5. Test API 호출(캐싱 진행)


정리

 

개발 환경에 따라 달라질 수 있지만

 

Spring Cache와 외부 캐시 서버(Redis, Apache Ignite 등)을 선택해야 하는 상황

외부 캐시 서버 사용을 추천드립니다.

 

Caching 데이터가 작거나, 긴급하게 Caching을 통해 성능을 개선하려고 한다면!?

Spring Cache을 도입해서 성능적인 이점을 얻을 수 있습니다.

 

댓글