Programming/Spring Boot

Spring Cache 캐시 추상화 기본적인 사용법 @Cacheable @CachePut @CacheEvict

Jan92 2022. 1. 18. 23:43

'Spring Boot 프로젝트 Cache 캐시 추상화 기본적인 사용법'

 

 

먼저 캐시(Cache)란,

캐시란 서버의 부담을 줄이고, 성능을 높이기 위해 사용되는 기술입니다.

예를 들어서 어떤 요청을 처리하는데 DB에서 조회하는 시간이 오래 걸리거나 계산이 복잡한 경우에 적용하여 요청 결과를 저장해 두고 가져옴으로써 빠르게 처리할 수 있는 기술입니다.

 

아래에서 살펴볼 Spring 프로젝트의 캐싱 사용법에서는 메서드에 캐싱을 적용함으로써 캐시에 보관된 정보로 메서드 실행 횟수를 줄여 주는 것을 볼 수 있습니다.

즉, 대상 메서드가 실행될 때마다 추상화되어 적용된 캐싱이 해당 메서드가 같은 인자로 이미 실행되었는지 확인하는 동작을 합니다.

해당 메서드가 실행되어 저장된 데이터가 존재한다면 실제 메서드를 실행하지 않고 저장된 결과를 반환하며, 해당 인자로 실행된 메서드의 데이터가 존재하지 않는다면 메서드를 실행하고, 그 결과를 캐싱한 뒤에 사용자에게 반환해서 다음번 호출 시에 사용할 수 있게 합니다.

 

* 이 접근 방법은 입력(인자)이 같으면 계속 호출되더라도 출력(결과)도 같다는 것을 보장하는 메서드에서만 적용 가능합니다.

 

 

캐시는 값을 저장해 두고 불러오기 때문에 반복적으로 동일한 결과를 반환하는 경우에 용이하며, 만약 매번 다른 결과를 돌려줘야 하는 상황에서 캐시를 적용한다면 캐시를 저장하거나 확인하는 작업 때문에 부하가 생겨서 오히려 성능이 떨어지게 됩니다.

 

 

 

 

로컬 캐싱과 글로벌 캐싱,

Spring Cache는 캐시 기능의 추상화를 지원하며 EhCache, memcache, Redis 등의 추가적인 캐시 저장소와 빠르게 연동하여 bean으로 설정할 수 있도록 도와줍니다. 만약 추가적인 캐시 저장소와 연결하지 않는다면 ConcurrentHashMap 기반의 Map 저장소가 자동으로 추가되며, 이때 사용되는 ConcurrentHashMap 기반의 Map 저장소는 로컬 캐시 저장소입니다. (간단한 캐시 처리가 필요한 경우 많이 사용됩니다.) 

 

로컬 캐싱은 서버 내부 저장소에 캐시 데이터를 저장하는 것입니다. 따라서 속도는 빠르지만 서버 간의 데이터 공유가 안된다는 단점이 있습니다.

반면 글로벌 캐싱은 서버 내부 저장소가 아닌 별도의 캐시 서버를 두어 서버에서 캐시 서버를 참조하는 것입니다.

캐시 데이터를 얻으려고 할 때마다 네트워크 트래픽이 발생하기 때문에 로컬 캐싱보다 속도는 느리지만 서버 간의 데이터를 쉽게 공유할 수 있기 때문에 로컬 캐싱의 적합성 문제와 중복된 캐시 데이터로 인한 서버 자원 낭비 등의 문제점을 해결할 수 있습니다.

 

 

***

캐싱 기능은 스프링 프레임워크의 다른 기능과 마찬가지로 추상화되어 있기 때문에 (캐시 구현체가 아닙니다.) 캐시 데이터를 저장하는 실제 스토리지를 사용해야 합니다. 즉, 캐시 추상화로 개발자가 캐시 로직은 작성하지 않아도 되지만 실제 캐시 스토어를 제공하는 것은 아닙니다.

 

+ 캐시 기능을 적용하는 것은 스프링이 제공해주는 어노테이션을 통해 적용하면 되기 때문에 신경 써야 할 부분은 적용할 캐시 기술을 선정하고 관련 설정을 넣어주는 것입니다.

 

 

* Spring Cache는 트랜잭션(@Transcation)과 마찬가지로 AOP를 이용하여 코드에 주는 영향을 최소화하면서 다양한 캐시 기능을 일관성 있게 사용할 수 있도록 해줍니다.

 

 


 

 

// gradle
implementation("org.springframework.boot:spring-boot-starter-cache")

// maven
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    <version>2.4.0</version>
</dependency>

캐시를 사용하기 위해서 먼저 의존성을 추가합니다. (gradle, maven)

 

 

 

@Configuration
@EnableCaching
public class CachingConfig { 
  // 하나의 저장소를 사용하는 경우
  @Bean
  public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager("memberCacheStore");
  }

  // 여러 개의 저장소를 추가할 경우
  @Bean
  public CacheManager cacheManager() {
    SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
    simpleCacheManager.setCaches(
        Arrays.asList(new ConcurrentMapCache("cacheStore1"), new ConcurrentMapCache("cacheStore2")));
    return simpleCacheManager;
  }
}

CacheManager를 사용하기 위한 Bean 등록입니다.

@EnableCaching 어노테이션으로 캐싱 기능을 활성화합니다.

 

 

***

@EnableCaching 어노테이션과 클래스의 경로에 stater 패키지가 있는 것만으로 동일한 ConcurrentMapCacheManager를 등록하기 때문에 별도의 Bean 등록이 필요하지 않지만, 여러 개의 저장소를 사용하는 등 추가적인 커스터마이징이 있을 수 있기 때문에 Config 클래스를 통해 빈을 등록합니다.

 

 

***

CacheManager Bean 등록 시 Spring에서 제공되는 캐시 매니저는 ConcurrentMapCacheManager, SimpleCacheManager 외에도 EhcacheCacheManager, CaffeineCacheManager, JCacheCacheManager, CompositeCacheManager 등이 있습니다.

 

 

캐시 매니저에 대한 자세한 내용은 아래 스프링 공식 문서를 참고하시면 됩니다.

https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache-store-configuration

 

 

 


 

'@Cacheable, @CachePut, @CacheEvict 세 가지 어노테이션 사용 방법'

 

 

  // 캐시 저장
  @Cacheable("memberCacheStore")
  public Member cacheable(String date) {
    System.out.println("cacheable 실행");
    ...
    return member;
  }

  // 캐시 저장 (Key를 지정한 경우)
  @Cacheable(value = "memberCacheStore", key = "#member.name")
  public Member cacheableByKey(Member member) {
    System.out.println("cacheableByKey 실행");
    ...
    return member;
  }

  // 조건부 캐시 저장 (With Condition)
  @Cacheable(value = "memberCacheStore", key = "#member.name", condition = "#member.name.length() > 5")
  public Member cacheableWithCondition(Member member) {
    System.out.println("cacheableWithCondition 실행");
    ...
    return member;
  }

@Cacheable

먼저 캐시를 가져오는 메서드(읽기 작업)에 사용되는 @Cacheable 어노테이션입니다.

해당 어노테이션의 경우 메서드의 특정 인자에 대한 결괏값을 캐시 저장소에 저장하고 같은 인자에 대한 결괏값은 실행하지 않고 캐싱 저장소에서 가져와서 반환해줍니다.

 

이때 메서드의 반환 값은 '캐시에 저장되는 내용'이 되고, 메서드의 파라미터는 '캐시 데이터의 키 생성에 사용'됩니다.

(캐시 데이터로써 반환 값이 저장될 때 키 정보도 함께 저장됩니다.)

 

 

 

첫 번째 key를 지정하지 않은 경우,

예를 들어 이달의 사원(지난달 가장 실적이 좋은 사원)을 찾는 메서드가 있습니다.

인자로 입력되는 date는 yyyy-MM 형식으로 입력되고, 같은 date에 대한 출력 결과인 Member(이달의 사원)은 바뀌지 않는다고 가정하겠습니다.

 

이때 date 값으로 '2022-01'이 처음 입력되었을 경우, 입력된 인자 '2022-01'에 대한 캐싱된 데이터가 없기 때문에 메서드가 실행되고 "cacheable 실행" 부분이 출력됩니다.

그리고 반환 값인 member가 캐시 저장소에 저장됩니다.

 

이어서 다시 date 값으로 '2022-01'을 넣고 요청을 합니다. 이때는 인자(date)인 '2022-01'에 대한 결괏값이 캐싱되어 있기 때문에 메서드가 실행되지 않고 캐시 저장소에 저장된 member 값이 반환됩니다.

"cacheable 실행" 이 출력되지 않습니다.

 

 

 

두 번째 key를 지정하는 경우,

두 번째는 키를 지정하는 경우입니다. 키 지정은 @Cacheable 어노테이션에 속성으로 key = "#name", key = "#member.name"와 같이 키를 지정할 수 있습니다.

(해당 방식은 SpEL - Spring Expression Language 문법이라고 합니다.)

 

예시에서는 member 객체의 필드 중 하나인 name을 key 값으로 지정했습니다.

따라서 만약 key인 name이 "jan"인 member 객체가 처음으로 들어왔을 때, 해당 객체 데이터는 캐싱되기 전이기 때문에 "cacheableByKey 실행"이 출력되고 "jan"이라는 name을 가진 member 객체 데이터는 캐싱됩니다.

 

그리고 이어서 name이 "jan"인 member 객체가 다시 요청되었을 때는 "cacheableByKey 실행"이 출력되지 않고 캐싱된 member 객체 데이터가 return 됩니다.

 

하지만 name이 "kavin"인 member 객체가 처음으로 들어온다면 "cacheableByKey 실행"이 다시 출력되고 name이 "kavin"인 member가 캐싱되게 됩니다.

 

 

 

세 번째 조건부 캐싱,

세 번째는 조건부 캐싱입니다.

인자에 따라 메서드를 캐싱하는 것이 적합하지 않을 때 사용할 수 있으며, @Cacheable 어노테이션에 condition 속성을 통해 적용할 수 있습니다. 

(condition = "#member.name.length() > 5" 옵션은 member 객체의 name 필드 값의 길이가 5를 초과하는 경우에만 캐싱되도록 한 것입니다.)

 

 

 

 

 

  // 캐시 저장 (Cacheable과 유사하게 실행 결과를 캐시에 저장하지만, 조회 시에 저장된 캐시 내용을 사용하지는 않고, 항상 메소드의 로직을 실행한다는 점에서 다르다.)
  @CachePut(value = "memberCacheStore", key = "#member.name")
  public Member cachePut(Member member) {
    System.out.println("cachePut 실행");
    ...
    return member;
  }

@CachePut

두 번째 @CachePut 어노테이션은 저장을 위한 기능입니다.

@Cacheable과 유사하게 실행 결과를 캐시에 저장하지만, 조회 시에 저장된 캐시 내용을 사용하지는 않고 항상 메서드의 로직을 실행한다는 점에서 다릅니다. 

 

메서드 실행에 영향을 주지 않고 캐시를 갱신해야 하는 경우에 사용됩니다.

즉, 같은 name을 가진 member 객체가 계속 들어오더라도 "cachePut 실행"이 계속해서 출력되면서 들어온 member 객체가 캐싱됩니다.

 

 

 

 

 

  // 캐시 제거
  @CacheEvict("memberCacheStore")
  public Member cacheEvict(String date) {
    System.out.println("cacheEvict 실행");
    ...
    return null;
  }

  // name 키 값을 가진 캐시만 제거
  @CacheEvict(value = "memberCacheStore", key = "#member.name")
  public Member cacheEvictByKey(Member member) {
    System.out.println("cacheEvictByKey 실행");
    ...
    return member;
  }

  // 캐시에 저장된 값을 모두 제거할 필요가 있을 때
  @CacheEvict(value = "memberCacheStore", allEntries = true)
  public Member cacheEvictAllEntries() {
    System.out.println("cacheEvictAllEntries 실행");
    ...
    return null;
  }

  // beforeInvocation 속성으로 메서드 실행 이후(기본값)나 이전에 제거를 해야하는지 지정할 수 있다.
  @CacheEvict(value = "memberCacheStore", beforeInvocation = true)
  public Member cacheEvictBeforeInvocation() {
    System.out.println("cacheEvictBeforeInvocation 실행");
    ...
    return null;
  }

@CacheEvict

마지막 @CacheEvict는 저장된 캐시를 제거할 때 사용됩니다.

 

 

 

첫 번째와 두 번째의 경우,

첫 번째의 경우 key가 되는 date 값에 대한 member 객체 데이터가 캐싱되어 있을 때 해당 key인 date 값으로 요청이 들어오면 메서드가 실행되며("cacheEvict 실행"이 출력됩니다.) 저장된 캐싱을 삭제합니다. 해당되는 key에 대한 데이터 값이 없더라도 메서드는 계속 실행됩니다.

 

두 번째 key 속성을 설정한 경우에도 마찬가지입니다.

 

 

 

세 번째 allEntries 속성의 경우,

세 번째 allEntreis 옵션의 경우 캐시에 저장된 값을 모두 제거할 필요가 있을 때 사용하는 옵션입니다.

 

 

 

마지막 beforeInvocation 속성의 경우,

beforInvocation 속성으로 메서드 실행 이후(기본값)나 이전에 캐시를 제거해야 하는지 지정할 수 있습니다.

메서드 실행 이전에 제거되는 경우(beforeInvocation = true)에는 메서드가 호출되기 전에 캐싱된 데이터가 항상 제거되기 때문에 제거가 메서드 결과에 의존하지 않는 경우에 유용합니다.