Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 28장 캐시 추상화

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.



28. 캐시 추상화


28.1 소개

3.1 버전부터 스프링 프레임워크는 기존 스프링 애플리케이션에 캐시를 투명하게 추가하도록 지원한다. 트랜잭션 지원과 비슷하게 캐시 추상화도 코드에 주는 영향을 최소화하면서 다양한 캐시 솔루션을 일관성 있게 사용할 수 있게 한다.

28.2 캐시 추상화 이해하기

핵심 부분에서 추상화는 Java 메서드에 캐싱을 적용함으로써 캐시에 보관된 정보로 메서드의 실행 횟수를 줄여준다. 즉, 대상 메서드가 실행될 때마다 추상화가 해당 메서드가 같은 인자로 이미 실행되었는지 확인하는 캐싱 동작을 적용한다. 해당 데이터가 존재한다면 실제 메서드를 실행하지 않고 결과를 반환하고 존재하지 않는다면 메서드를 실행하고 그 결과를 캐싱한 뒤에 사용자에게 반환해서 다음번 호출 시에 사용할 수 있게 한다. 이 방법을 통해 비용이 큰 메서드(CPU든 IO든) 해당 파라미터로는 딱 한 번만 실행할 수 있고 다시 메서드를 실행하지 않고도 결과를 재사용할 수 있다. 호출자가 어떤 방해도 받지 않고 캐싱 로직은 투명하게 적용된다.

Important
물론 이 접근방법은 얼마나 많이 호출하든지 간에 입력(혹은 인자)이 같으면 출력(결과)도 같다는 것을 보장하는 메서드에서만 동작한다.


캐시 추상화를 사용하려면 개발자가 두 가지 관점을 신경을 써야 한다.

  • 캐싱 선언 - 캐시되어야 하는 메서드와 정책을 정한다
  • 캐시 구성 - 데이터를 저장하고 읽을 기반 캐시

스프링 프레임워크의 다른 서비스와 마찬가지로 캐싱 서비스는 추상화되어 있고(캐시 구현체가 아니다) 캐시 데이터를 저장하는 실제 스토리지를 사용해야 한다. 즉, 캐시 추상화로 개발자가 캐시 로직은 작성하지 않아도 되지만 실제 스토어를 제공하는 것은 아니다. 통합할 수 있는 두 가지 캐시는 JDK java.util.concurrent.ConcurrentMap와 Ehcache이다. 다른 캐시 스토어나 제공자를 연결하려면 Section 28.6, “다른 백엔드 캐시에 연결하기(plugging-in)”를 참고해라.

캐시 vs 버퍼
"버퍼"와 "캐시"라는 용어는 서로 바꿔가며 쓸 수 있다. 하지만 둘은 의미가 다르다. 버퍼는 빠른 엔티티와 느린 엔티티 중간에 데이터를 임시로 저장하는데 보통 사용된다. 어떤 부분이 대기 해야 해서 다른 성능에 영향을 준다면 작은 청크가 아니라 데이터의 전체 블록을 한 번에 옮기게 해서 이 성능 저하를 완화한다. 데이터는 버퍼에서만 읽고 쓰인다. 게다가 버퍼는 버퍼를 알고 있는 최소 한 부분에서는 가시적이다. 반면에 캐시는 숨겨져 있고 관련된 부분이 캐싱 된다는 것을 알지도 못한다. 같은 데이터를 빠르게 여러 번 읽음으로써 성능을 높인다. 캐시와 버퍼의 차이점에 대한 자세한 설명은 여기에 나와 있다.


28.3 선언적인 어노테이션 기반의 캐싱

캐싱 선언으로 추상화가 두가지 Java 어노테이션을 제공한다. @Cacheable와 @CacheEvict로 캐시군(cache population)과 캐시 만료(cache eviction)를 실행하는 메서드를 제공한다. 각 어노테이션을 자세히 살펴보자.

28.3.1 @Cacheable 어노테이션

이름이 암시하듯이 @Cacheable는 캐시할 수 있는 메서드를 지정하는데 사용한다. 즉, 이 어노테이션을 사용한 메서드는 결과를 캐시에 저장하므로 뒤이은 호출(같은 인자일 때)에는 실제로 메서드를 실행하지 않고 캐시에 저장된 값을 반환한다. 가장 간단한 형식으로는 어노테이션이 붙은 메서드와 연관된 캐시의 이름만 있으면 어노테이션을 선언할 수 있다.

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

앞의 코드에서 findBook 메서드는 books라는 캐시와 연결된다. findBook 메서드를 호출할 때마다 해당 호출이 이전에 실행된 적이 있는지 캐시가 확인하고 반복해서 실행하지 않는다. 하나의 캐시만 선언하는 대부분의 경우 어노테이션은 여러 이름을 지정해서 한 캐시를 여러 번 사용할 수 있다. 이 경우 메서드를 실행하기 전에 각 캐시를 확인할 것이고 최소 하나의 캐시에 저장되어 있다면 해당 값을 반환할 것이다.

Note
이 기능은 기본적으로는 비활성화되어 있다.
@Cacheable({ "books", "isbns" })
public Book findBook(ISBN isbn) {...}


28.3.1.1 기본 키 생성

캐시는 본질적으로 키-밸류 저장소이므로 캐시된 메서드를 호출할 때마다 해당 키로 변환되어야 한다. 캐시 추상화는 다음 알고리즘에 기반을 둔 KeyGenerator를 사용한다.

  • 파라미터가 없으면 0을 반환한다.
  • 파라미터가 하나만 있으면 해당 인스턴스를 반환한다.
  • 파라미터가 둘 이상이면 모든 파라미터의 해시를 계산한 키를 반환한다.

이 접근은 객체를 리플랙션하는 hashCode()처럼 일반적인 키를 가진 객체와 잘 동작한다. 이러한 경우가 아니고 분산 혹은 유지(persistent)되는 환경이라면 객체가 hashCode를 보관하지 않도록 전략을 변경해야 한다. 사실 JVM 구현체나 운영하는 환경에 따라 같은 VM 인스턴스에서 hashCode를 다른 객체에서 재사용할 수 있다.

다른 기본 키 생성자를 제공하려면 org.springframework.cache.KeyGenerator 인터페이스를 구현해야 한다. 이를 구성하고 나면 전용 키 생성 전략(아래 참고)를 지정하지 않은 모든 선언에서 이 생성자를 사용할 것이다.

28.3.1.2 커스텀 키 생성 선언

캐싱은 일반적이므로 대상 메서드는 캐싱 구조에 간단하게 매핑할 수 없는 다양한 시그니처를 가질 가능성이 높다. 이는 여러 인자중 일부만 캐싱하기에 적합한(나머지 인자는 메서드 로직에만 사용된다) 대상 메서드가 있는 경우에 명확해 지려는 의도이다. 예를 들면 다음과 같다.

@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed

간단히 말하면 두 boolean 인자는 책을 찾는데 영향을 주고 캐시에는 사용하지 않는다. 게다가 둘 줄 하나만 중요하고 다른 인자는 중요하지 않다면?

이러한 경우 @Cacheable 어노테이션으로 사용자가 key 속성으로 키를 생성하는 방법을 지정할 수 있다. 개발자는 사용할 인자(또는 중첩된 속성)를 선택하거나 작업 실행이나 어떤 코드도 작성하지 않고 인터페이스도 구현하지 않고 임의의 메서드를 호출하려고 SpEL를 사용할 수 있다. 메서드는 코드가 달라짐에 따라 시그니처도 달라질 것이므로 기본 생성자 외의 접근을 추천한다. 기본 전략은 일부 메서드에서 동작할 것이지만 모든 메서드에서 동작할 가능성은 별로 없다.

아래 여러 가지 SpEL 선언의 예시가 있다. SpEL에 익숙하지 않다면 익숙한 것을 사용하거나 Chapter 7, Spring 표현 언어 (SpEL)를 읽어봐라.

@Cacheable(value="books", key="#isbn"
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(value="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

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

위의 코드는 특정 인자나 그 속성이나 임의의 (stsatic) 메서드를 선택하기가 얼마나 쉬운지 보여준다.

28.3.1.3 조건부 캐싱

때로는 메서드를 항상 캐싱하는 것이 적합하지 않을 수 있다.(예를 들어 인자에 따라 다를 수 있다.) 캐시 어노테이션은 true나 false가 되는 SpEL 표현식을 받는 conditional 파라미터로 이러한 기능을 지원한다. 이 표현식이 true이면 메서드를 캐시하고 false이면 메서드가 캐시 되지 않은 것처럼 동작해서 해당 값이 캐시가 되었든 인자가 무엇이든 간에 매번 실행된다. 다음은 name인 32자보다 적은 경우에만 캐시 되는 간단한 예시이다.

@Cacheable(value="book", condition="#name.length < 32")
public Book findBook(String name)


28.3.1.4 사용 가능한 캐싱 SpEL 평가 컨텍스트(evaluation context)

각 SpEL 표현식은 전용 context를 다시 평가한다. 파라미터를 구성하는 것에 추가로 프레임워크는 인자 이름 같은 메타데이터와 관련된 전용 캐시를 제공한다. 다음 표는 컨텍스트에서 사용 가능한 요소가 나와 있어서 키와 조건부(다음 섹션 참고) 계산에 사용할 수 있다.


Table 28.1. Cache SpEL에서 사용 가능한 메타데이터

이름 위치 설명 예시
methodName root object 호출되는 메서드의 이름 #root.methodName
method root object 호출되는 메서드 #root.method.name
target root object 호출되는 대상 객체 #root.target
targetClass root object 호출되는 대상 클래스 #root.targetClass
args root object 대상을 호출하는데 사용한 인자(배열) #root.args[0]
caches root object 현재 실행된 메서드 캐시의 컬렉션 #root.caches[0].name
argument name evaluation context 메서드 인자의 이름. 어떤 이유로든 이름을 사용할 수 없다면(예: 디버깅 정보가 없는 경우) a<#arg>에서 인자 이름을 사용할 수도 있고 #arg은 (0부터 시작하는) 인자의 인덱스를 의미한다. iban나 a0 (p0를 사용하거나 별칭으로 p<#arg> 형식을 사용할 수 있다.).

.

28.3.2 @CachePut 어노테이션

메서드 실행에 영향을 주지 않고 캐시를 갱신해야 하는 경우 @CachePut 어노테이션을 사용할 수 있다. 즉, 메서드를 항상 실행하고 그 결과를 (@CachePut 옵션에 따라) 캐시에 보관한다. @CachePut은 @Cacheable와 같은 옵션을 지원하고 메서드 흐름 최적화보다는 캐시 생성(population)에 사용해야 한다.

같은 메서드에 @CachePut와 @Cacheable 어노테이션을 사용하는 것은 둘이 다른 동작을 가지므로 보통 권장하지 않는다. @Cacheable는 캐시를 사용해서 메서드 실행을 건너뛰고 @CachePut는 캐시 갱신을 하려고 실행을 강제한다. 이는 특정한 코너케이스가 아니라면(두 어노테이션이 서로 조건이 겹치지 않는 경우) 의도치 않은 동작이 발생할 수 있어서 이러한 선언은 하지 말아야 한다.

28.3.3 @CacheEvict 어노테이션

캐시 추상화로 캐시 스토어의 생성 뿐만 아니라 제거도 할 수 있다. 이 과정은 캐시에서 오래되거나 사용하지 않는 데이터를 제거하는데 유용하다. @Cacheable와는 달리 @CacheEvict 어노테이션은 캐시를 제거(eviction)하는 메서드를 구분하는데 즉, 캐시에서 데이터를 제거하는 트리거로 동작하는 메서드다. 다른 캐시 어노테이션과 마찬가지로 @CacheEvict는 동작할 때 영향을 끼치는 하나 이상의 캐시를 지정해야 한다. @CacheEvict에서 키나 조건을 지정해야 할 수 있지만 딱 하나의 엔트리(키에 기반을 둔)가 아니라 제거를 할 캐시의 범위를 나타내는 allEntries 파라미터를 추가로 사용할 수 있다.

@CacheEvict(value = "books", allEntries=true)
public void loadBooks(InputStream batch)

한 지역의 전체 캐시를 모두 지워야 할 때 이 옵션을 편리하게 사용할 수 있다. 각 엔트리를 하나씩 지우는 것이 아니라(비효율적이라 시간이 오래 걸린다) 위 예제처럼 한 번에 모든 엔트리를 제거한다. 이 시나리오에서 지정한 키에 적용되지 않는 것은 프레임워크가 무시할 것이다.(한 엔트리가 아니라 전체 캐시가 제거된다)

beforeInvocation 속성으로 메서드 실행 이후(기본값)나 이전에 제거를 해야 하는 지를 지정할 수도 있다. 메서드 실행 이후에 실행되는 경우는 다른 어노테이션과 같은 의미를 가져서 메서드가 성공적으로 완료되면 캐시에서 동작(여기서는 제거)이 실행된다. 메서드가 실행되지 않거나(캐시 되어서) 예외가 던져지면 제거가 실행되지 않는다. 메서드 실행 이전에 실행되는 경우(beforeInvocation=true)에는 메서드가 호출되기 전에 항상 제거가 발생해서 제거가 메서드 결과에 의존하지 않는 경우에 유용하다.

@CacheEvict를 void 메서드에 사용할 수 있다는 것은 중요한 부분이다. 메서드가 트리거로 동작하므로 반환값은 무시한다.(캐시와 상호작용하지 않으므로) 이는 캐시에 데이터를 넣거나 갱신해서 그 결과가 필요한 @Cacheable와는 다른 점이다.

28.3.4 @Caching 어노테이션

@CacheEvict나 @CachePut처럼 같은 계열의 어노테이션을 여러 개 지정해야 하는 경우가 있는데 예를 들어 조건이나 키 표현식이 캐시에 따라 다른 경우이다. 안타깝게도 자바는 이러한 선언을 지원하지 않지만 감싸진(enclosing) 어노테이션을 사용해서(이 경우에는 @Caching) 우회할 수 있다. @Caching에서 중첩된 @Cacheable, @CachePut, @CacheEvict를 같은 메서드에 다수 사용할 수 있다.

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


28.3.5 캐시 어노테이션 활성화하기

캐시 어노테이션을 선언하는 것만으로 자동으로 동작이 실행되지 않는다는 점은 중요하다. 스프링의 다른 기능처럼 선언적으로 기능을 활성화해야 한다.(이는 캐시에 문제가 있다고 의심된다면 코드의 모든 어노테이션을 지우는 대신 설정에서 딱 한 줄만 지워서 캐시를 비활성화할 수 있다는 의미이다.) 실제로는 이 선언으로 스프링이 캐시 어노테이션을 처리하도록 한다.

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:cache="http://www.springframework.org/schema/cache"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
  <cache:annotation-driven />
</beans>

애플리케이션에 추가된 캐시의 동작 방식을 AOP로 변경할 다양한 옵션을 네임스페이스로 지정할 수 있다. 설정은 tx:annotation-driven의 설정과 (의도적으로) 비슷하다.

Table 28.2. <cache:annotation-driven/> 설정

속성 기본값 설명
cache-manager cacheManager 사용할 캐시 관리자의 이름. 위 예제처럼 캐시 관리자의 이름이 cacheManager가 아닐 때만 지정한다.
mode proxy 기본값인 "proxy" 모드는 스프링의 AOP 프레임워크로 어노테이션이 붙은 빈을 프락시한다.(프락시의 개념에 맞게 프락시를 통해서 오늘 메서드 호출에만 적용한다.) 다른 모드인 "aspectj"는 영향받은 클래스를 스프링의 AspectJ 캐싱 관점으로 위빙한다. (대상 클래스의 바이트코드를 메서드 호출에 적용되도록 수정한다.) AspectJ 위빙을 사용하려면 로딩 타임 위빙(또는 컴파일 타임 위빙) 활성화와 마찬가지로 클래스패스에 spring-aspects.jar가 있어야 한다.(로드 타임 위빙을 설정하는 방법은 Section 8.8.4.5, “스프링 설정”를 참고해라.)
proxy-target-class false proxy 모드에만 적용된다. @Cacheable나 @CacheEvict 어노테이션이 붙은 클래스에 어떤 종류의 캐싱 프락시를 생성할지를 제어한다. proxy-target-class 속성을 true로 설정하면 클래스에 기반을 둔 프락시를 생성한다. proxy-target-class가 false이거나 proxy-target-class 속성을 생략하면 표준 JDK 인터페이스에 기반을 둔 프락시를 생성한다. (다양한 프락시 타입에 대한 자세한 내용은 Section 8.6, “프록싱 메카니즘”를 참고해라.)
order Ordered.LOWEST_PRECEDENCE @Cacheable나 @CacheEvict 어노테이션이 붙은 빈에 적용된 캐시 어드바이스의 순서를 정의한다.(AOP 어드바이스 순서에 관한 규칙은 Section 8.2.4.7, “어드바이스 순서”를 참고해라.) 순서를 지정하지 않으면 AOP 하위시스템이 어드바이스의 순서를 결정한다.

.

Note
는 선언된 애플리케이션 컨텍스트에서 빈에 붙은 @Cacheable/@CacheEvict만 찾는다. 즉, DispatcherServlet의 WebApplicationContext에 를 선언하면 서비스는 빼고 컨트롤러에서만 @Cacheable/@CacheEvict 빈을 확인한다. 자세한 내용은 Section 16.2, “DispatcherServlet”를 참고해라.


Tip
스프링은 인터페이스에 어노테이션을 붙이는 것과는 반대로 구현(concrete) 클래스에만(구현 클래스의 메서드) @Cache* 어노테이션을 붙이기를 권장한다. 물론 인터페이스에(혹은 인터페이스 메서드) @Cache* 어노테이션을 붙일 수 있지만, 인터페이스에 기반을 둔 프락시를 사용할 때만 원하는 대로 동작한다. Java 어노테이션이 인터페이스를 상속받지 않는다는 점은 클래스나 위빙에 기반을 둔 프락시(proxy-target-class="true"나 mode="aspectj")를 사용한다면 프락시나 위빙 인프라가 캐시 설정을 인식하지 못한다는 것을 의미힌다. 그리고 해당 객체는 캐시 프락시로 감싸지지 않을 것이므로 이는 확실히 좋지 않다.


Note
프락시 모드(기본값이다)에서는 프락시로 호출되는 외부 메서드만 가로챌 수 있다. 즉, 대상 객체 내의 메서드가 대상 객체의 다른 메서드를 호출하는 자기호출(self-invocation)은 해당 메서드에 @Cacheable 어노테이션이 붙어있더라도 런타임에서 실제 캐싱 되지 않을 것이다.(이러면 aspectj의 사용을 고려해 봐라.)


메서드 가시성(visibility)과 @Cacheable/@CachePut/@CacheEvict
프락시를 사용할 때 public 가시성을 가진 메서드에만 @Cache* 어노테이션을 적용해야 한다. protected, private, package-visible 메서드에 이러한 어노테이션을 붙이면 에러는 발생하지 않지만 어노테이션이 붙은 메서드에는 구성한 캐시 설정이 보이지 않는다. public이 아닌 메서드에 어노테이션을 사용해야 한다면 AspectJ(아래 참고)를 고려해 봐라.(AspectJ는 바이트코드 자체를 바꾼다.)


28.3.6 커스텀 어노테이션의 사용

캐싱 추상화로 어떤 메서드가 캐시 생성과 제거를 실행하는지 확인하기 위한 자신만의 어노테이션을 사용할 수 있다. 이는 캐시 어노테이션을 중복으로 설정할 필요가 없어져서(특히 키나 조건을 지정할 때 유용하다) 템플릿 메커니즘처럼 아주 편리하고 외부 임포트(org.springframework)가 코드에서 사용할 수 없는 경우에 유용하다. stereotype 어노테이션과 유사하게 @Cacheable와 @CacheEvict 어노테이션 모두 다른 어노테이션에 어노테이션을 붙일 수 있는 메타 어노테이션으로 사용할 수 있다. 즉, @Cacheable 선언을 커스텀 어노테이션을 대체할 수 있다.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(value=“books”, key="#isbn")
public @interface SlowService {
}

앞의 코드에서 @Cacheable 어노테이션이 붙어있는 SlowService 커스텀 어노테이션을 정의했다. 이제 다음 코드를 바꿀 수 있다.

@Cacheable(value="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

이 코드를 다음과 같이 바꿀 수 있다.

@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@SlowService가 스프링 어노테이션이 아님에도 컨테이너가 런타임시에 자동으로 어노테이션 선언을 찾아서 그 의미를 이해할 수 있다. 앞에서 얘기한 대로 어노테이션 주도 동작을 활성화해야 한다.

28.4 선언적 XML에 기반을 둔 캐싱

어노테이션이 선택사항이 아니고(소스에 접근권한이 없거나 외부 코드가 없는 경우) 선언적인 캐싱에 XML을 사용할 수 있다. 그래서 캐싱하려고 메서드에 어노테이션을 붙이는 대신 대상 메서드와 캐싱 지시어를 외부에서 지정한다.(선언적인 트랜잭션 관리 advice와 유사하다) 앞의 예제를 다음과 같이 바꿀 수 있다.


<bean id="bookService" class="x.y.service.DefaultBookService"/>


<cache:advice id="cacheAdvice" cache-manager="cacheManager">
  <cache:caching cache="books">
    <cache:cacheable method="findBook" key="#isbn"/>
    <cache:cache-evict method="loadBooks" all-entries="true"/>
  </cache:caching>
</cache:advice>


<aop:config>
  <aop:advisor advice-ref="cacheAdvice" pointcut="execution(* x.y.BookService.*(..))"/>
</aop:config>
...
// 캐시 관리자 정의는 생략한다

앞의 설정에서 bookService는 캐싱이 가능하다. 여기서 적용되는 캐시 동작은 cache:advice 정의에 은닉화되어 있어서 loadBooks 메서드가 데이터를 제거하는 동안 캐시에 데이터를 보관하는데 findBooks 메서드를 사용하도록 한다. 두 정의 모두 books 캐시에 동작한다.

aop:config 정의는 AspectJ 포인트컷 표현식(자세한 내용은 Chapter 8, 스프링의 관점 지향 프로그래밍에 있다.)을 사용해서 프로그램의 적절한 시점에 캐시 어드바이스를 적용한다. 앞의 예제에서 BookService의 모든 메서드를 검토해서 캐시 어드바이스를 적용한다.

선언적인 XML 캐싱이 어노테이션에 기반을 둔 모든 모델을 지원하므로 어노테이션과 XML 간의 변경은 아주 쉽다. 게다가 한 애플리케이션 내에서 둘을 함께 사용할 수도 있다. XML에 기반을 둔 접근방법은 대상 코드를 건드리지 않지만, XML의 특성상 약간 더 장황하다. 캐싱 대상이 되는 클래스의 오버 로딩된 메서드를 다루는 경우 method 인자가 적절한 식별자가 아니므로 적합한 메서드를 구별하려면 추가적인 노력이 필요하다. 이러면 AspectJ 포인트컷으로 대상 메서드를 골라내서 적절한 캐싱 기능을 적용할 수 있다. 하지만 XML을 사용하면 패키지/그룹/인터페이스 범위의 캐싱을 적용하고(AspectJ 포인트컷 때문에) 템플릿같은 정의를 생성하기가(앞의 예제에서 cache:definitions cache 속성으로 대상 캐시를 정의한 것처럼) 더 쉽다.

28.5 캐시 스토리지 구성

캐시 추상화는 JDK ConcurrentMap에 기반을 둔 스토리지와 ehcache 라이브러리의 스토리지와 통합할 수 있다. 이 스토리지를 사용하려면 CacheManager를 적절히 선언하면 된다. CacheManager로 Cache를 제어하고 관리하고 스토리지에서 Cache를 가져와서 사용할 수 있다.

28.5.1 JDK ConcurrentMap에 기반을 둔 Cache

JDK에 기반을 둔 Cache 구현체는 org.springframework.cache.concurrent 패키지 아래 존재한다. 이 구현체로 Cache 스토어에 기반을 둔 ConcurrentHashMap를 사용할 수 있다.

<!-- generic cache manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
  <property name="caches">
  <set>
    <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
    <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/>
  </set>
  </property>
</bean>

앞의 코드는 default와 books라는 이름의 중첩된 Concurrent Cache 구현체를 위한 CacheManager를 생성하는데 SimpleCacheManager를 사용한다. 이름은 각 캐시에 직접 설정했다.

애플리케이션이 캐시를 생성하므로 캐시는 애플리케이션의 생명주기를 따른다. 이는 테스트나 간단한 애플리케이션 같은 기본적인 사용 사례에 적합하다. 이 캐시는 잘 확장되고 아주 빠르지만 어떤 관리도 제공하지 않고 보관 기능이나 제거 계약이 존재하지 않는다.

28.5.2 Ehcache에 기반을 둔 Cache

Ehcache 구현체는 org.springframework.cache.ehcache 패키지 아래 있다. 마찬가지로 Ehcache 구현체를 사용하려면 CacheManager를 적절히 선언하면 된다.

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache"/>


<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="ehcache.xml"/>

이 설정은 스프링 IoC 내에서 ehcache를 구동하고(ehcache 빈을 통해서) 전용 CacheManager 구현체에 연결한다. ehcache에 특화된 전체 설정은 ehcache.xml 리소스에서 읽는다.

28.5.3 기반 스토어없이 캐시 다루기

때로는 환경을 바꾸거나 테스트를 할 때 실제 기반 캐시를 구성하지 않고 캐시를 설정해야 하는 경우가 있다. 이는 유효하지 않은 구성이므로 런타임시에 캐시 인프라가 적절한 스토어를 찾을 수 없어서 예외가 던져질 것이다. 이러한 경우 캐시 선언을 제거하는 대신(지루한 작업이다) 캐싱을 수행하지 않는 간단한 더비 캐시를 연결할 수 있다. 즉 캐시된 메서드가 매번 실행되도록 강제한다.

<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
  <property name="cacheManagers"><list>
    <ref bean="jdkCache"/>
    <ref bean="gemfireCache"/>
  </list></property>
  <property name="addNoOpCache" value="true"/>
</bean>

앞의 CompositeCacheManager는 여러 CacheManager를 체이닝하고 있고 설정한 캐시 관리자가 다루지 않는 모든 정의에 no op 캐시를 추가한다. (addNoOpManager 플래그로) 즉 (앞에서 설정한) jdkCache나 gemfireCache에서 찾지 못한 모든 캐시 정의는 no op 캐시로 다뤄서 아무런 정보를 저장하지 않아서 대상 메서드가 매번 실행되도록 한다.

28.6 다른 백엔드 캐시에 연결하기(plugging-in)

기반 저장소로 사용할 수 있는 많은 캐시 제품이 분명히 존재한다. 이를 연결하려면 CacheManager와 Cache 구현체를 제공해야 하지만 안타깝게도 사용할 수 있는 표준은 존재하지 않는다. 이는 ehcache 클래스가 보여주었듯 스토리지 API 상위의 캐시 추상화 프레임워크에 매핑되는 간단한 adapter가 되는 클래스여야 하므로 꽤 어려운 얘기로 들린다. CacheManager 클래스 대부분은 작성해야 할 실제 매핑만 남겨둔 보일러 플레이트 코드를 다루는 AbstractCacheManager와 같은 org.springframework.cache.support 패키지의 클래스를 사용할 수 있다. 스프링과의 통합을 제공하는 라이브러리가 이 약간의 설정 차이를 조만간 메꿔주기를 기대하고 있다.

28.7 TTL/TTI/Eviction policy/XXX 기능을 어떻게 설정하는가?

캐시 프로바이더로 직접 설정한다. 케시 추상화는 캐시 구현체가 아니다. 사용하는 제품은 아마 다른 제품이 지원하지 않는(JDK ConcurrentHashMap 예제를 봐라) 다양한 데이터 정책과 여러가지 토폴로지(topologies)를 지원할 것이다. 캐시 추상화에서 이를 노출하는 것은 의존하는 지원체가 없어서 쓸모없을 것이다. 이러한 기능은 캐시를 설정할 때나 캐시의 네이티브 API로 기반 캐시에서 직접 제어해야 한다.

2014/11/08 05:45 2014/11/08 05:45