Spring Cache Abstraction

스프링에서는 org.spring.framework.cache.Cache 그리고 org.springframework.cache.CacheManager 를 사용하여 캐시 구현 기술에 종속되지 않도록 추상화된 서비스를 제공하고 있습니다. 그렇기 때문에 환경이 바뀌거나 적용할 캐시 기술을 변경하여도 애플리케이션 코드에 영향을 주지 않습니다.

Spring Cache 설정

@EnableCaching 추가

스프링에서 제공하는 Annotation 기반의 캐시 서비스를 이용하려면 Bean 스캔 범위 안에서 @EnableCaching 을 설정해주어야합니다.

@EnableCaching  
@SpringBootApplication  
public class StressTestBasicApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(StressTestBasicApplication.class, args);  
    }  
}

조금 더 딥하게 들어가보면 @EnableCaching 내부 CachingConfigurationSelector 라는 ImportSelector 로 인해 ProxyCachingConfiguration 설정파일이 동작하게 됩니다. 결국 BeanFactoryCacheOperationSourceAdvisor 라는 Bean 이 최종적으로 등록되어 AOP 기반 캐시 서비스를 이용할 수 있게 됩니다.

CacheManager Bean 설정

@EnableCaching 으로 설정을 추가한 후, 캐시를 관리해줄 CacheManager 타입의 Bean 을 등록해주어야 합니다. 스프링에서 기본적으로 제공하는 CacheManager 들은 아래와 같습니다. 더 자세한 내용은 공식문서 를 참고하면 좋을 것 같습니다.

  • ConcurrentMapCacheManager: Java 의 ConcurrentHashMap 을 사용하는 캐시 매니저
  • SimpleCacheManager: 기본적으로 제공하는 캐시가 없어 사용할 캐시를 직접 등록하여 사용하기 위한 캐시 매니저
  • CompositeCacheManager: 1개 이상의 캐시 매니저를 사용하도록 지원해주는 혼합 캐시 매니저
  • EhCacheCacheManager: Java 의 유명한 캐시 프레임워크 중 하나인 EhCache를 지원하는 캐시 매니저 EhCache 3.x 부터는 JSR-107 을 완벽준수하기 때문에 JCacheCacheManager 를 참조
  • CaffeineCacheManager: Java 8로 Guava 캐시를 재작성한 캐시 매니저
  • JCacheCacheManager: JSR-107 기반의 캐시를 사용하는 캐시 매니저

JSR- 107

JSR 은 Java Specification Request 을 의미합니다. 즉, JSR-107 은 자바 기반 애플리케이션에서 캐싱을 표준화하기 위한 인터페이스와 규칙을 정의한 명세서를 의미합니다. JSR-107 을 따르는 대표적인 Java Caching API 가 JCache 입니다.

외부 의존성이 필요한 EhCacheCacheManager, CaffeineCacheManager, JCacheCacheManager 를 제외한 나머지 CacheManager 들이 JDK 에 있는것을 확인하실 수 있습니다.

어떠한 설정도 하지 않았을 때의 기본으로 등록되는 CacheManager 는 ConcurrentMapCacheManager 입니다. CacheManager 가 등록되는 순서를 보려면 CacheAutoConfiguration, CacheConfigurationImportSelector, CacheConfigurations, SimpleCacheConfiguration 에서 BreakPoint 를 찍고 분석해보시면 됩니다.

Spring Cache 사용

Spring 에서 사용할 수 있는 Annotation 기반 캐시는 공식문서 에서 더 자세하게 살펴볼 수 있습니다.

@Cacheable

@Cacheable 어노테이션으로 메서드의 결과를 캐싱할 수 있습니다. 메서드의 두번째 호출부터는 실제로 메서드를 호출하지 않고도 값을 반환할 수 있습니다. 아래 코드에서 sayHello 가 캐시이름, testString 의 값 이 Key 가 되게 됩니다. 당연히 Value 는 메서드의 리턴값이 됩니다.

@Cacheable("sayHello")  
public String sayHello(String testString) {  
    
}

또한, @Cacheable 값으로 여러 캐시이름을 명시하여, 메서드의 결과를 여러개의 캐시에 캐싱할 수도 있습니다.

@Cacheable({"sayHello", "sayHello2"})  
public String sayHello() {  
    
}

만약 메서드의 매개변수가 없다면 0 이라는 디폴트 값을 Key 로 사용하게 됩니다. 아래의 코드는 sayHello 라는 캐시이름에 Key 가 0 인 캐시를 저장 및 조회하는 코드가 됩니다.

@Cacheable("sayHello")  
public String sayHello() {  
    
}

기본적으로 메서드의 매개변수가 하나면, 해당 매개변수를 캐시의 Key 사용합니다. 그리고 메서드의 매개변수가 두개 이상이면 모든 매개변수들의 hashCode 를 조합하여 Key 를 만들어 Key 로 사용하게 됩니다. 해당 내용은 여기서 확인할 수 있습니다.

@Cacheable("book")  
public String findBook(String isbn, Long bookNo) {  
    
}

매개변수가 여러개인 상황에서, 하나의 매개변수만을 Key 로 사용하고 싶거나, 원하는 매개변수들만을 조합하여 Key 로 사용하고 싶을땐 SpEL(Spring Expression Language) 을 사용할 수 있습니다. 또한, SpEL 로 Wrapper 타입의 매개변수의 특정 프로퍼티값에도 접근하여 key 로 사용할 수 있습니다.

@Cacheable(cacheNames = "book", key = "#isbn")  
public String findBook(String isbn, Long bookNo, int count) {  
 
}
 
@Cacheable(cacheNames = "book", key = "{#isbn,#bookNo}")  
public String findBook(String isbn, Long bookNo, int count) {  
    
}
 
@Cacheable(cacheNames = "book", key = "#isbn.value")  
public String findBook(ISBN isbn, Long bookNo, int count) {  
    
}

마지막으로 @Cacheable 은 조건부 캐싱을 지원합니다. 아래와 같이 Key 가 특정 Condition 을 만족할 경우에만 캐싱할 수 있도록 작성할 수 있습니다.

@Cacheable(cacheNames = "book", key = "#isbn.value", condition = "#isbn.value.equals(#bookNo.toString())")  
public void condionalBook(ISBN isbn, Long bookNo) {  
 
}

@CachePut

새로운 캐시로 갱신만 하고 싶다면 @CachePut 어노테이션을 사용할 수 있습니다. @Cacheable 은 캐싱 여부에 따라 메서드 호출여부가 결정되는 반면, @CachePut 은 메서드가 항상 호출되고, 그 결과만 새로운 캐시로 갱신한다는 점에서 차이가 있습니다.

@CacheEvict

캐시의 삭제는 @CacheEvict 를 사용할 수 있습니다. 특정 이름의 캐시를 지우고 싶다면 아래와 같이 작성해줄 수 있습니다.

@CacheEvict(cacheNames = "book")  
public void deleteBook() {  
}

또한, SpEL 로 Reference 인스턴스의 프로퍼티에 접근하여 특정 캐시의 Key 에 해당하는 캐시만 지워줄 수 있습니다.

@CacheEvict(cacheNames = "book", key = "#isbn.value")  
public void deleteBook(ISBN isbn) {  
}

특정 캐시에 저장된 모든 값들을 지우고 싶으면 아래와 같이 작성해줄 수 있습니다.

@CacheEvict(cacheNames = "book", allEntries = true)  
public void deleteBook() {  
}

만약, 일정한 주기로 특정 캐시를 지우고 싶다몀 @Scheduled 와 병행하여 사용할 수 있습니다. 물론 @EnableSchedulingScheduling 을 활성화시키는 것을 잊지 말아야합니다.

@Scheduled(initialDelay = 10, fixedRate = 3, timeUnit = TimeUnit.SECONDS)  
@CacheEvict(cacheNames = "book", allEntries = true)  
public void scheduledDeleteCache() {  
}