(스프링) 13장 데이터 캐싱하기


**캐싱(Caching)은 자주 필요한 정보를 저장하는 방법으로, 해당 정보가 필요할 때 사용될 수 있도록 한다.


1.  캐시 지원하기


스프링의 캐시 추상화 지원은 두가지 형태를 가진다.

  • 애너테이션 주도 캐싱
  • XML선언 캐싱
빈의 캐싱 에너테이션 적용 전에는 스프링의 애너테이션 주도 캐싱 지원을 해야 한다.
자바 설정을 사용할 경우, @EnableCaching을 설정 클래스 중 하나에 추가하여 애너테이션 주도 캐싱을 사용한다.

<@EnableCaching을 사용하여 애너테이션 주도 캐싱 사용하기>

@Configuration
@EnableCaching <= 캐시 활성화
public class CachingConfig{

   @Bean
   public CacheManager cacheManager(){  <= 캐시 매니저 선언
      return new ConcurrentMapCacheManager();
   }

 

XML로 애플리케이션을 설정할 경우, 스프링의 캐시네임스페이스에서 <cache:annotation-driven> 요소를 사용하여 애너테이션 주도 캐싱을 사용한다.


<cache:annotation-driven>으로 애너테이션 주도 캐싱 사용하기

<cache:annotation-driven />
<bean id="cacheManger" class="org.springframework.cache.concurrent.ConcurrentMapCacheManager" />

캐시 상태와 애너테이션에 따라 애스펙트는캐시의 값을 가져오고, 캐시에 값을 추가하고, 캐시의 값을 삭제한다.


위의 두 코드는 캐시매니저 빈을 선언한다. 캐시 매니저는 스프링 캐시 추상화의 핵심이며, 여러 개의 캐싱 구현체 중 하나를 사용항 통합된다.
이때, ConcurrentMapCacheManager가 선언된다. 이 간단한 캐시매니저는 캐시 저장소로 java.util.concurrent.ConcurrentHashMap을 사용한다. 캐시 저장소는 메모리 기반이고, 애플리케이션 라이프 사이클에 엮어 있으므로 큰 규모의 애플리케이션에 이상적인 선택은 아니다.


1.1 캐시 매니저 설정하기

 다섯 개의 캐시 매니저 구현체를 지원한다.
  • SimpleCacheManager
  • NoOpCacheManager
  • ConcurrentMapCacheManager
  • CompositeCacheManager
  • EhCacheManager

스프링 3.2 는 캐시 프로바이더(cache provider)를 기반으로 JCache를 사용하는 다른 캐시 매니저를 제공한다. 스프링 데이터는 두 개 이상의 추가적인 캐시 매니저를 제공한다.
  • RedisCacheManager(스프링 데이터 Redis)
  • GemfireCacheManager(스프링 데이터 GemFire)

캐시매니저를 선택할 떄 스프링 캐시 추상화를 위해 여러가지 선택을 한다. 선택은 사용하고자 하는 캐시 프로바이더에 따라 다르다. 각각은 애플리케이션에 다른 형태의 캐싱을 제공하며, 일부는 다른 것에 비해서 좀 더 상품화되어 있다. 비록 선택은 데이터가 어떻게 캐싱되느냐에 따라 달라지지마, 스프링에서 캐싱 규칙을 선언한 방법과는 관련이없다.

<Ehcache로 캐싱하기>


*자바 설정에서 EhCacheCacheManager 설정하기

@Configuration
@EnableCaching
public class CachingConfig{
 
  @Bean
   public EhCacheCacheManager cacheManager(CacheManager cm){
<=EhCacheCacheManager 설정
    return new EhCacheCacheManager(cm);
  }
   
 @Bean
  public EhCacheCacheManagerFactoryBean ehcache(){

    EhCacheManagerFactoryBean ehCacheFactoryBean=
            new EhCacheManagerFactoryBean();

   ehCacheFactoryBean.setConfigLocation(
            new ClassPathResource("com/habuma/spittr/cache/ehcache.xml"));
     return ehCacheFactoryBean;
      }
}



-->ehcache()메소드는 EhCacheManagerFactoryBean 인스턴스를 생성하고 반환한다. 팩토리 빈 때문에 스프링 애플리케이션 컨텍스트에 등록된 빈은 EhCacheManagerFactoryBean인스턴스가 될 수 없고, CacheManager 인스턴스보다는 EhCacheCacheManager에 주입되는 것이 더 적합하다.
그리고 스프링에서 설정할 빈보다 EhCache설정에 더 적합하다. EhCache는 XML을 위한 자신의 설정 스키마를 정의하고, 스키마에 대한 XML파일에서 지정되는 캐싱을 설정한다.
EhCacheCacheManagerFactoryBean을 만드는 과정에서 EhCache 설정 XML이 있을 위치가 필요하다. 지금 setConfigLocation() 메소드를 호출하고, classpath루트와 관련 있는 Ehcache XML 설정 위치를 지정하기 위해 ClassPathResource를 전달한다.


<ehcache.xml>

<ehcache>
  <cache name="spittleCache"
             maxBytesLocalHeap="50m"
             timeToLiveSeconds="100">
  </cache>
</ehcache>

----> spittleCache라는 이름의 캐시를 50MB의 최대 힙 저장소, 100초의 유효기간을 사용한다.




<캐싱을 위해 Redis 사용하기>

캐시 엔트리에서 키는 동작과 파라미터를 나타내고 값이 제공되는 키-값 쌍 외의 다른 것을 가지지 않는다. 
따라서 Redis는 스프링 캐싱 추상화를 위한 캐시 엔트리를 저장하기 위한 용도로 사용될 수 있으며, 스프링 데이터 Redis는 CacheManager의 구현체인 RedisCacheManager을 제공한다. RedisCacheManager는 Redis에서 캐시 엔트리를 저장하기 위해서 RedisTemplate를 통해 Redis서버를 가지고 동작한다.

RedisCacheManager를 사용하기 위해서는 RedisTemplate빈과 RedisConnectionFactory구현 빈이 필요하다. 


***<Redis 서버에서 캐시 엔트리를 저장하기 위한 캐시매니저 설정하기>

@Configuration
@EnableCaching
public class CachingConfig{

  @Bean
   public CacheManager cacheManager(RedisTemplate redisTemplate){ <=Redis캐시매니저 빈
        return new RedisCacheManager(redisTemplate);
   }

  @Bean
  public JedisConnectionFactory redisConnectionFactory(){ <=Redis 연결 팩토리 빈
    JedisConnectionFactory jedisConnectionFactory=
               new JedisConnectionFactory();
    jedisConnectionFactory.afterPropertiesSet();
    return jedisConnectionFactory;
  }


  @Bean
   public RedisTemplate<String, String> redisTemplate(    <=RedisTemplate빈
                      ReidsConnectionFactory redisCF){

        RedisTemplate<String, String> redisTemplate=
               new RedisTemplate<String, String>();
       redisTemplate.setConnectionFactory(redisCF);
       redisTemplate.afterPropertiesSet();
       return redisTemplate;
   }
}


<다중 캐시 매니저 사용하기>

사용할 캐시 매니저에 문제가 있거나 한 개 이상의 캐시 매니저를 선택해야 할 기술적인 이유가 존재한다면, 스프링의 CompositeCacheManager를 사용한다.
CompositeCacheManager는 한 개 이상의 캐시 매니저를 사용하여 설정되며, 이전에 캐시된 값을 찾기 위해서 시도했던 방법들을 모두 반복하여 사용한다.


**<CompositeCacheManager는 캐시 매니저 리스트를 반복한다.>

@Bean
public CacheManager cacheManager( net.sf.ehcache.CacheManager cm, javax.cache.CacheManager jcm)
{
   CompositeCacheManager cacheManager=new CompositeCacheManager();
   <=CompositeCacheManager 생성
   
  List<CacheManager> managers= new ArrayList<CacheManager>();
  managers.add(new JCacherCacheanager(jcm));
  managers.add(new EhCacheCacheManager(cm));
  managers.add(new RedisCacheManager(redisTemplate()));
  cacheManager.setCacheManagers(managers);   <==개별 캐시 매니저 추가

  return cacheManager;
}



2.  캐싱을 위한 애너테이션 메소드


스프링의 캐싱 추상화는 애스펙트에 대해 넓게 적용된다. 스프링에서 캐싱을 사용할 때, 애스펙트는 한 개 이상의 스프링 캐싱 애너테이션을 시작하기 위해 만들어진다.


**<스프링은 캐싱 규칙을 위한 네 개의 애너테이션을 제공한다.>


  • @Cacheable : 스프링ㅇ 메소드 사용전에 반환 값을 위해 캐시를 살펴 본다. 값이 존재할 경우, 캐시된 값이 반환된다. 값이 존재하지 않는다면,메소드가 실행되고 반환된 값이 캐시에 남는다.
  • @CachePut : 스프링이 캐시에 메소드 반환값을 저장한다. 캐시는 메소드 실행전에는 체크되지 않으며, 메소드는 항상 실행한다.
  • @CacheEvict : 스프링이 캐시에서 한 개 이상의 엔트리를 내쫒는다
  • @Caching : 다른 캐싱 애너테이션을 여러 번 즉시 적용하기 위해 사용할 수 있는 그룹 애너테이션이다.


단일 메소드에서 사용될 떄, 애너테이션에 의해서 미리 정해진 캐싱 동작은 오직 그 메소드에 대해서만 적용된다. 그러나 애너테이션이 클래스 레벨이면 캐싱 동작은 그 클래스의 모든 메소드에 적용된다.


2.1 캐시 채우기

@Cacheable은 먼저 캐시의 엔트리를 찾고, 매칭되는 엔트리가 발견되면 메소드 호출을 미리 수행한다. 매칭되는 엔트리가 발견되지 않으면, 메소드가 호출되고 반환 값은 캐시안에 남는다.
반면, @CachePut은 캐시 내에 매칭값을 체크하지 않으며, 타깃 메소드가 사용될 수 있도록하고 반환값을 캐시에 추가한다.

**<@Cacheable과 @CachePut은 애트리뷰트 공통 세트를 공유한다.>


  • 애트리뷰트       타입
  1.   value           String[]  : 사용할 캐시 명
  2.  condition       String   :    SpEL 표현식, 값이 false이면 캐싱은 메소드 호출에 적용되지 않는다.
  3. key               String   :  맞춤형 캐시 키를 계산하기 위한 SpEL표현식
  4. unless            String  : SpEL표현식, 값이 true이면 반환 값이 캐시에 남지 않게 된다.


만약에 특정 Spittle이 인기 있게 사용되고 자주 요청되면, 데이터베이스에서  반복되어 가져오려 하므로 시간과 리소스가 낭비된다. @Cacheable를 사용하여 findOne() 메소드를 애너테이션하여 다음코드에서 나타낸 것처럼 Spittle이 캐싱되고, 데이터베이스로의 불필요한 접근을 피할 수 있도록 한다.

**<캐시에 값을 저장하고 값을 가져오기 위해 @Cacheable 사용하기>

@Cacheable("spittleCache")
public Spittle findOne(long id){
  
   try{
      return jdbcTemplate.queryForObject(
               SELECT_SPITTLE_BY_ID,
                new SpittleRowMapper(),
                id);
      }catch(EmptyResultDataAccessException e){
       return null;
        }
}


-->> findOne()이 호출될 때 캐싱 애스펙트는 그 호출을 가로채고, spittleCache라는 캐시의 이전 반환 값을 찾는다.  캐시 키는 findOne()메소드로 전달되는 id 파라미터다. 그 키에 대한 값이 있으면, 발견값은 반환되고 메소드는 실행되지 않는다. 반대로 값이 발견되지 않으면, 메소드는 호출되고 반환 값은 캐시에 남아서, 다음 번 findOne()이 호출될 때를 대기한다.


<캐시에 값 넣기>

@CachePut애너테이션 된 메소드는 항상 호출되고 반환 값이 캐시에 남는다. 이 방법은 누가 요청하기 전에 캐시를 미리 로드하는 간단한 방법을 제공한다.

예를들면, Spittle이 save()메소드를 통해서 SpittleRepository에 저장될 떄, 곧 요청 받을 확률이 매우 높다. 따라서 save()가 호출될 때 캐시로 Spittle을 넣고, findOne()을 호출해 두면, 누군가 찾을 떄 바로 적용할 수 있다.

예)
@CachePut("spittleCache")
Spittle save(Spittle spittle);

-->save()가 호출될 때, Spittle을 저장하기 위해 필요한 동작을 수행한다. 그리고 반환된 Spittle은 spittleCache캐시에 저장한다.



<맞춤형 캐시 키 만들기>

@Cacheable과 @CachePut은 기본 키를 SpEL표현식으로 부터 추출된 것으로 교체하기 위한 키 애트리뷰트를 가진다. 유효한 SpEL표현식은 동작하지만, 캐시에 저장된 값과 관련된 키를 평가하기 위한 표현식을 사용해야 한다.


**<스프링은 캐시 규치글 정의하기 위해 특별히 여러 개의 SpEL표현식을 제공한다.>

  • #root.args : 인자는 캐시된 메소드에 배열로 전달된다.
  • #root.caches : 메소드가 수행된 캐시이며 배열과 같다.
  • #root.target : 타깃 객체를 나타낸다.
  • #root.targetClass : 타깃 객체의 클래스다. #root.target.class의 숏컷이다.
  • #root.method : 캐시된 메소드다.
  • #root.methodName : 캐시된 메소드 명이다. #root.method.name의 숏컷이다.
  • #result : 메소드 호출에서 반환된 값이다.(@Cacheable에 대해선 비유효)
  • #Argument : 메소드 인자명(#argName 같은)또는 인자의 인덱스(#a0 또는 #p0같은)

save메소드에 대해, 반환 되는 Spittle에서 얻은 id 프로퍼티인 키가 필요하다. #result표현식은 반환된 Spittle을 제공한다. 여기서 키 애트리뷰트를 #result.id로 설정하여 id프로퍼티를 참조한다.

@CachePut(value="spittleCache" , key="#result.id")
Spittle save(Spittle sppittle);

 


댓글

이 블로그의 인기 게시물

(18장) WebSocekt과 STOMP를 사용하여 메시징하기

(네트워크)폴링방식 vs 롱 폴링방식

(ElasticSearch) 결과에서 순서 정렬