본문 바로가기
Study/spring

Spring Cache Abstraction 정리

by 유경호 2021. 2. 9.
반응형

캐시 추상화(Cache Abstraction)

스프링 캐싱 추상화는 스프링 어플리케이션에 캐싱을 투명하게 적용할 수 있게 도와주는 기술이다.

최소한의 코드 작성으로 일관된 다양한 캐싱 솔루션을 제공한다.

캐시(Cache)의 특징

  • 캐시 적용의 목적은 성능 향상에 있다.
  • 반복적인 동일한 작업에 적용할 수 있다.
    • 매번 다른 결과를 돌려줘야 하는 작업에 적용하면 오히려 캐시 로직 때문에 성능이 저하된다.
  • 캐시 저장소에 저장해둔 내용이 변경되는 상황에 잘 대처해야한다.
    • 데이터의 일관성을 유지하여야함. 항상 올바른 값을 반환하도록 대처해야함.
      → 예) 캐시 데이터의 원본 데이터가 변경된다면 변경된 데이터를 반환하도록 대체
  • 여러 위치에 적용이 가능하다.
    • DB 조회와 관련된 캐시라면 데이터 액세스 기술에 적용
    • JPA나 하이버네이트 같은 ORM에도 1차 캐시라는 기능이 있다.
    • DB 내부적으로 제공하는 캐시 기능
    • 웹 서버 또는 전용 캐시 장비가 제공하는 웹 캐시 기능을 이용해 특정 요청의 static resources(HTML과 같은)를 통째로 캐시 데이터로 활용할 수도 있음.
    • 스프링 캐시 추상화는 빈이 가지고 있는 메소드의 결과를 캐시에 저장해두어 동일한 파라미터가 전달될 때 메소드를 실행하지 않고 저장해놨던 캐시 데이터를 반환함.
캐시 vs 버퍼
버퍼와 캐시는 서로 다른 목적으로 쓰이지만 상충하는 부분이 있다.
전통적으로 버퍼는 엔티티 간에 임시 데이터 저장소로 쓰인다. 한 쪽에서 데이터가 넘어올 때가지 기다려야 한다. 이것은 성능에 영향을 미칠 수 있다. 버퍼는 이러한 점을 데이터를 한 번에 모았다가 넘기는 방식으로 완화했다. 작은 덩어리로 나눠서 보내기 보다는, 그리고 데이터의 쓰기와 읽기는 버퍼를 통해 한 번씩만 가능하다. 그리고 버퍼는 해당 버퍼를 인지하고 있는 반대 쪽에서만 데이터를 받을 수 있다.

반면에 캐시는 어디서든 캐싱이 일어나는 것을 감지할 수 있다. 이것은 성능을 향상시킬 수 있다 그러나 동시에 데이터에 접근하는 일도 일어난다.

참고
https://en.wikipedia.org/wiki/Cache_(computing)#The_difference_between_buffer_and_cache

어노테이션 기반 캐싱 적용

스프링 캐시 추상화는 어노테이션을 메소드에 적용하여 AOP를 통해 캐싱 로직을 제공한다. 아래와 같은 자바 어노테이션을 지원하며 해당 어노테이션들을 붙여 캐싱을 적용할 수 있다.

  • @Cacheable: Triggers cache population.
  • @CacheEvict: Triggers cache eviction.
  • @CachePut: Updates the cache without interfering with the method execution.
  • @Caching: Regroups multiple cache operations to be applied on a method.
  • @CacheConfig: Shares some common cache-related settings at class-level.

애노테이션을 이용한 캐시 기능 설정

  • @Cacheable과 @CacheEvict 등을 사용하려면, @Configuration 클래스에 @EnableCaching 애노테이션을 추가해주기만 하면 된다.
@Configuration
@EnableCaching
public class AppConfig {
}

@Cacheable 어노테이션

@Cacheable는 메소드에 캐싱을 적용할 수 있도록 해준다. 메소드의 특정 인자에 대한 결과값을 캐시 저장소에 저장하고 같은 인자에 대한 결과값은 메소드를 실행하지 않고 캐싱 저장소에서 가져와 반환 해준다.

  • 메소드의 반환값: 캐시에 저장되는 내용
  • 메소드 파라미터: 캐시 데이터의 키 생성에 사용됨
  • 캐시 데이터로써 반환값이 저장될 때 키 정보도 함께 저장됨.

어노테이션을 붙일 때는 해당 메소드와 연관된 캐시 이름을 지정해주는 것이 필요하다. 아래의 예시를 보자.

@Cacheable("books")
public Book findBook(ISBN isbn) {...}
  • 캐시에 저장되는 정보를 구분하기 위해 캐시 이름을 사용함
  • 별 다른 키 생성 설정이 없는 경우, 스프링 캐시 추상화에서 제공하는 SimpleKeyGererator가 키를 생성해줌
  • 메소드의 파라미터가 없는 경우는 캐시 서비스의 기본 키 값 생성 구현 방식에 의해 0이라는 키를 지정한다.
  • 메소드의 파라미터 값이 여러 개인 경우
  • 디폴트 : 모든 파라미터의 hashCode() 값을 조합해서 키로 만든다.
  • @Cacheable 애노테이션의 key 엘리먼트를 이용해 키 값으로 사용할 파라미터를 지정해줄 수 있다. key 엘리먼트는 SpEL을 이용해 키 값을 지정한다. SpEL을 이용해 파라미터의 특정 프로퍼티 값을 키로 사용할 수도 있다.

대부분 하나의 캐시 이름을 이용하지만, 아래와 같이 두 개 이상의 캐시 이름을 사용할 수도 있다.

@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
  • 각각의 캐시 이름을 메소드가 호출되기 전에 검사함
  • 각각의 캐시 이름 중 어느곳이든 맞는 캐시 데이터가 있다면 메소드를 호출하지 않고 캐시 데이터를 반환함

@CacheEvict annotation

스프링 캐시 추상화는 데이터를 저장하는 것 뿐아니라 eviction에 대해서도 지원을 한다. 해당 프로세스는 오래된 데이터나 사용되지 않는 데이터를 지우는데 유용하다.

  • 캐시는 적절한 시점에 제거돼야 한다.
    • 캐시는 메소드를 실행했을 때와 동일한 결과가 보장되는 동안에만 사용돼야하고 메소드 실행 결과와 캐시 값이 달라지는 순간 제거돼야 한다.
  • 캐시를 제가하는 2가지 방법
    • 일정한 주기로 캐시를 제거하는 것
    • 캐시에 저장한 한 값이 변경되는 상황을 알 수 있는 경우 이를 이용해 캐시를 제거할 수 있다.
  • 캐시 제거에 사용될 메소드에 @CacheEvict 애노테이션을 붙이면 된다.
@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)
  • allEntires에 true를 부여하면 캐시에 모든 데이터를 제거한다.

@CachePut Annotation

  • 캐시에 값을 업데이트하는 용도로 사용한다.
  • 캐시의 값이 저장만 되며 캐시 데이터를 읽는 기능은 하지 않는다.
  • 메소드의 실행 결과를 캐시에 저장하지만, 저장된 캐시의 내용을 사용하진 않는다. 즉 항상 메소드를 실행한다.
  • 한 번에 캐시에 많은 정보를 저장해두는 작업이나, 다른 사용자가 참고할 정보를 생성하는 용도로만 사용되는 메소드에 이용할 수 있다.
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)

@Caching Annotation

  • 같은 타입의 어노테이션을 여러 개를 적용할 필요가 있을 때 사용할 수 있다.
  • 다수의 condition을 갖거나, 캐시를 여러 개 사용할 때 키 생성 전략이 다르거나 할 때 유용하게 사용될 수 있음
  • @Cacheable, @CachePut, @CacheEvict의 다중 사용을 지원한다.

예시)

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

@CacheConfig annotation

위의 어노테이션에 들어가는 설정들(캐시 이름, 키생성전략, key resolver 등)을 메소드 각각에 적용할 수도 있지만 @CacheConfig을 사용해 클래스에 설정을 부여할 수도 있다. 이를 이용하면 각 메소드에 반복적으로 적용할 설정들을 한 번에 해줄 수 있다.

@CacheConfig("books") 
public class BookRepositoryImpl implements BookRepository {

    @Cacheable
    public Book findBook(ISBN isbn) {...}
}
  • CacheName, CacheManager, KeyGenerator, CacheResolver를 글로벌하게 설정해줄 수 있음

키 생성

기본 키 생성 전략

캐시는 근본적으로 key-value 구조를 가진 저장소이다. 캐시가 적용된 메소드들은 각각 캐시에 엑세스하기 위한 적절한 키를 생성하게 된다. 캐싱 추상화는 simple KeyGenerator 를 사용한다.

  • 패러미터 객체에 적절한 hashCode(), equals() 구현이 있다면 수정할 필요 없음
  • 스프링 4.0 이전 버전에서는 org.springframework.cache.interceptor.DefaultKeyGenerator 사용
    • 여러 패러미터를 가진 키를 생성할 때 hashCode()만 사용하여 예기치 않은 키 충돌을 일으킴
    • 4.0 이후로는 SimpleKeyGenerator 사용

SimpleKeyGenerator는 아래와 같은 알고리즘으로 동작한다.

  • if no params are given, return SimpleKey.EMPTY.
  • If only one param is given, return that instance.
  • If more than one param is given, return a SimpleKey that contains all parameters.

이는 대부분의 상황에 이상없이 활용할 수 있다. 하지만 키생성 전략 변경을 원하는 경우 org.springframework.cache.interceptor.KeyGenerator 인터페이스를 구현하여 빈 등록하면 된다.

 

커스텀 키 생성

  • 메소드의 모든 패러미터를 써서 캐시 키를 만들기엔 적절하지 않을 때 사용
    • 일부만 캐시 키로 쓰고, 나머지는 메소드 내부 로직에만 사용하는 경우
  • SpEL을 사용해서 특정 패러미터, 혹은 그 패러미터의 프로퍼티를 지정
  • 코드 베이스가 확장됨에 따라 메소드의 패러미터도 복잡해지기 때문에 추천되는 방법
    • 기본 키 생성 전략이 모든 메소드에 적용되는 일은 드뭄
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

SpEL을 사용해서 키 생성에 사용할 파라미터들을 찝어낼 수 있게 해준다.

// 키로 사용할 파라미터를 명시
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

// 파라미터의 특정 프로퍼티 값을 키로 사용
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

키 생성 알고리즘을 커스텀하거나 샤딩이 필요한 경우에는 KeyGenerator를 구현한 구현체를 빈 등록해 사용할 수 있다.

@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

캐싱 동기화

캐싱 데이터 동기화 처리

멀티 쓰레드 환경에서는 특히 웹 서버에선 다수의 사용자들이 특정 메소드에 동시에 접근하는 경우게 많다. 스프링 캐시 추상화의 기본설정은 이러한 부분에 대해서 처리를 해주질 않도록 설계돼있다. 만약 동기화 처리를 원하는 경우에는 sync라는 속성에 true값을 부여하면 된다.

@Cacheable(cacheNames="foos", sync=true)
public Foo executeExpensiveOperation(String id) {...}
  • 캐시 구현체가 thread safe 하지 않을 때 사용
  • sync를 걸어두면 단 하나의 쓰레드만 해당 메소드를 처리한다.
  • 그 후의 요청들은 해당 작업이 끝날 때까지 기다리게 됨.
  • 모든 캐시 라이브러리가 지원하는 것은 아니라고함.
  • 모든 CacheManager는 핵심 프레임워크가 지원하는 한에서 기능을 제공하기 때문에 문서를 잘 살펴볼 것

조건부 캐싱

캐싱이 모든 시점에 올바를 수 없다. 주어진 파라미터에 따라 메소드의 결과값이 바뀔 수 있기 때문인데, 스프링 캐시 추상화는 이런 점을 완화하기 위해 SpEL을 사용하여 조건에 따라 캐시 데이터를 저장하고 검색할 수 있게 해준다.

아래와 같이 condition 속성에 SpEL을 사용해 조건식을 넣어 사용할 수 있다.

@Cacheable(cacheNames="book", condition="#name.length() < 32")
 public Book findBook(String name)
  • condition의 결과가 true이면 캐시가 적용된채로 작동
  • condition의 결과가 false인 경우 캐시가 적용되지 않은 것처럼 동작한다.
  • 위의 예시로 설명하자면, name의 길이가 32아래인 경우에만 캐시를 적용함.

추가적으로 unless를 사용하면 메소드의 특정 결과값에 대해서 조건식을 사용해 캐싱 적용을 거부할 수 있다.

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback")
public Book findBook(String name)
  • 연산 조건이 true 인 경우에 캐싱되지 않는다

캐싱 추상화는 java.util.Optional 사용을 지원한다. Optional을 사용하여 값을 리턴하는 메소드일 경우 Optional에 값이 present될 경우에만 캐싱이 작용한다.

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)
  • #resultOptional 래퍼 객체가 아닌 담고 있는 객체이다.

 

캐시 저장소 설정

  • 스프링 자체적으로 지원하는 캐시들은 해당하는 CacheManager 구현체를 bean 등록
  • 그 외에는 CacheManager, Cache 직접 구현 필요
    • AbstractCacheManager 와 같은 추상 클래스들을 사용하여 보일러플레이트 줄이기 가능

각 캐시 스토리지 설정법은 공식문서를 참고

 

JDK ConcurrentMap

  • cacheManager: org.springframework.cache.support.SimpleCacheManager

Ehcache

  • cacheManager: org.springframework.cache.ehcache.EhCacheCacheManager
  • ehcache: org.springframework.cache.ehcache.EhCacheManagerFactoryBean

Caffeine

  • cacheManager: org.springframework.cache.caffeine.CaffeineCacheManager

Guava

  • cacheManager: org.springframework.cache.guava.GuavaCacheManager

GemFire

JSR-107

  • cacheManager: org.springframework.cache.jcache.JCacheCacheManager

참고

스프링 공식 문서 - docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache

반응형