Spring Cache에 앞서 Cache에 대해 정의하자면
CPU가 자주 사용하는 정보, 데이터 및 프로그램을 임시로 저장합니다.
데이터가 필요할 때 CPU는 더 빠른 데이터 액세스를 위해 자동으로 가장 가깝고 빠른 캐시 메모리에 액세스합니다.
캐시 메모리에서 데이터가 발견되면 캐시 적중이 발생합니다.
캐시 적중을 통해 프로세서는 데이터를 빠르게 검색하여 시스템의 전반적인 응답 속도 및 효율성을 높일 수 있습니다.
대신 비교적 저장 공간이 작고 비용이 비싼 단점이 있습니다.
이어서 Spring Cache는 스프링 프레임워크에서 제공하는 캐싱 추상화 라이브러리로,
메서드의 결과나 데이터를 캐시에 저장하여 반복적인 계산이나 데이터베이스 액세스를 피하고 성능을 향상시키는 기능을 제공합니다.
스프링에서 사용하는 캐시는 대부분 자바 플랫폼에 대한 규격을 기술한 JSR(Java Specification Requests)-107 을 따르고 있으며 이를 따르는 캐시 구현체는 모두 추상화를 지원합니다.
추상화 단계에서는 다중 스레드, 다중 프로세스 환경을 별도로 고려하고 있지는 않으며 이는 각 구현체에서 처리되고 있습니다.
'도중에 변경될 일이 없는 DB 조회 값' 또는 '자주 조회되는 데이터'에 캐시를 적용한다면 성능 향상, DB 접근 및 시스템 부하 감소의 이점을 얻을 수 있습니다.
로컬 캐시
로컬(캐시를 저장한 서버)에서만 사용하는 캐시.
주로 서버의 메인 메모리에 위치하여 외부 시스템과 트랜잭션 비용이 들지 않기 때문에 속도가 빠르다는 장점이 있지만 분산 서버 구조에선 캐시를 공유하기 어려운 단점이 있습니다.
글로벌 캐시
여러 서버에서 접근할 수 있는 캐시 서버를 구축하여 사용하는 방식입니다.
주로 Redis와 같은 별도의 서버로 운영되기 때문에 서버 간 데이터 공유에 용이하지만
네트워크를 통해 접근해야하기 때문에 로컬 캐시에 비해 상대적으로 느린 단점이 있습니다.
Spring에서는 간단한 어노테이션 만으로 캐시 추상화를 사용할 수 있습니다.
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
캐시 생성을 담당하는 어노테이션입니다.
어노테이션을 선언한 후에는 해당 캐시명을 선언해야 합니다.
최초 캐시 생성 시에는 메서드가 정상적으로 실행되나
이후 findBook 메서드가 호출될 때마다 반복할 필요가 없는 지 확인하기 위해 캐시를 확인합니다.
정상적으로 캐시가 조회되면 메서드는 실행되지 않고 저장된 캐시의 값이 반환됩니다.
캐시는 본질적으로 key-value 저장소이므로 캐시 조회를 위해선 캐시 액세스에 적합한 키로 변환되어야 합니다.
기본키 생성은 아래의 알고리즘을 사용합니다.
SimpleKey.EMPTY
반환@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
모든 매개변수가 메서드에 영향을 미치지만 캐시에는 쓸모가 없는 경우 '사용자 지정 키 생성' 방식을 고려할 수 있습니다.
아래와 같이 @Cacheable 매개 변수에 key를 명시하여 직접 key를 선언합니다.
@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)
키 생성을 담당하는 알고리즘이 복잡한 경우 아래 인터페이스를 구현하여 커스텀 키 생성기를 제공합니다.
org.springframework.cache.interceptor.KeyGenerator
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
기본적으로 캐시 추상화는 캐시에 대해 락을 걸지 않으며
이는 동일한 값이 여러 번 계산되어 캐싱의 목적을 무산시킬 수 있습니다.
sync
옵션을 통해 대상 항목이 캐시에 업데이트될 때 까지 다른 스레드를 차단하는 기능을 제공합니다.
@Cacheable(cacheNames="foos", sync=true)
public Foo executeExpensiveOperation(String id) {...}
@Cacheable(cacheNames="book", condition="#name.length() < 32")
public Book findBook(String name)
때로는 매개변수의 특정 조건에서만 캐시 생성할 때가 있습니다.
condition 옵션을 통해 위 예시와 같이 name 변수의 길이가 32보다 작을 경우에만 캐시 생성을 합니다.
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback")
public Book findBook(String name)
condition
과 달리 unless
는 메서드가 호출된 후에 평가되며
위 예시는 메서드의 반환값 (Book)이 hardback이 아닐 경우에만 캐싱을 진행합니다.
키 또는 조건부 연산에 사용살 수 있는 표현식은 아래 표와 같습니다.
이름 | 위치 | 설명 | 예 |
---|---|---|---|
methodName | 루트 개체 | 호출되는 메서드의 이름 | #root.methodName |
method | 루트 개체 | 호출되는 메서드 | #root.method.name |
target | 루트 개체 | 호출되는 대상 객체 | #root.target |
targetClass | 루트 개체 | 호출되는 대상의 클래스 | #root.targetClass |
args | 루트 개체 | 대상을 호출하는 데 사용되는 인수 (배열) | #root.args[0] |
caches | 루트 개체 | 현재 메서드가 실행되는 캐시 컬렉션 | #root.caches[0].name |
매개변수명 | 평가식 | 메서드 매개변수명 | #{매개변수명} |
result | 평가식 | 메서드 호출 결과 | #result |
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
메서드 실행을 방해하지 않고 캐시를 업데이트 하고 싶은 경우 @CachePut
어노테이션을 사용합니다.
이 어노테이션은 메서드는 항상 실행이 되며 그 결과는 선언한 옵션에 따라 캐시에 저장됩니다.
@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)
캐시에서 자주 사용하지 않는 데이터를 삭제할 경우에는 @CacheEvict를 통해 캐시를 삭제합니다.
위 예제에서는 allEntries
옵션을 통해 books
캐시의 모든 엔트리를 삭제하는 작업을 진행합니다.
beforeInvocation=true
옵션을 추가한다면 메서드 실행 전에 항상 실행이 되기 때문에
메서드의 실행 여부, 예외 여부 상관없이 항상 제거를 진행할 수 있습니다.
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
경우에 따라 하나의 메서드에 동일한 유형(@CacheEvice or @CachePut)을 여러번 선언해야하는 경우에는 위 예시와 같이 @Caching
어노테이션을 통해 어노테이션 그룹화가 가능합니다.
지금까지 캐시 추상화가 제공하는 개념 및 어노테이션을 알아봤습니다.
다음에는 이를 구현한 Provider 들을 비교해보고 간단하게 설정해보도록 하겠습니다.