목차

Memcached란?

  • 무료로 사용할 수 있는 오픈소스이며 분산 메모리 캐싱 시스템이다. 
  • 데이터 베이스의 부하를 줄여 동적 웹 어플리케이션의 속도개선을 위해 사용되기도 함
  • DB나 API호출 또는 렌더링 등으로부터 받아오는 결과 데이터를 작은 단위의 key - value 형태로 메모리에 저장하는 방식
  • Memcached는 필요량보다 많은 메모리를 가졌을 때, 시스템으로부터 메모리를 사용하고 필요로하는 메모리가 부족한 경우 이를 더 쉽게 가져다 사용할 수 있도록 만들어 줌
  • Memcached 사용여부에 따른 메모리 운영방식

Memcached를 사용하지 않을 경우

각 노드는 완벽하게 독립적임

이 경우 고전적으로 사용되던 방식으로 총 캐시 크기가 웹팜(여러 대를 사용해서 웹사이트를 구축한 형태)의 실제 용량의 일부분으로만 사용이 가능하다는 점에서 낭비가 심하다. 각각의 서버에 할당된 캐시 크기만큼 사용할 수 있으므로 웹팜의 캐시 사이즈는 128MB이지만 각 서버에서 사용할 수 있는 사이즈는 65MB이다.

Memcached를 사용할 경우

Memcached를 사용할 경우 Memcached로 묶인 모든 서버는 동일한 가상 메모리 풀을 공유한다. 이것은 특정한 항목이 주어졌을 때, 전체 웹 클러스터에서 항상 동일한 위치에 저장되고 검색되어짐을 뜻한다. 또한 응용프로그램에 대한 수요가 증가하여 서버증설에 대해 필요성을 느낄 때, 정기적으로 접근되어져야 하는 데이터의 관점에서도 수요가 증가한다고 볼 수 있다.

Memcached를 적용하면 분산 메모리 캐시를 적용하게 되는 것이므로 캐싱을 통해 DB나 API 호출에 대한 횟수를 줄일 수 있고 이로 인해 응용프로그램의 수요나 DB 데이터 접근에 대한 부하를 줄여 성능을 향상할 수 있다.

Redis란

redis는 오픈소스로서 데이터베이스(NOSQL DBMS)로 분류가 되기도 하고 Memcached와 같이 인메모리 솔루션으로 분류되기도 한다.

성능은 memcached에 버금가면서 다양한 데이터 구조체를 지원함으로써 Message Queue, Shared memory, Remote Dictionary 용도로도 사용될 수 있으며, 이런 이유로 인스타그램, 네이버 재팬의 LINE 메신져 서비스, StackOverflow,Blizzard,digg 등 여러 소셜 서비스에 널리 사용되고 있다. 

 

NoSQL관점에서 봤을 때 redis는 가장 단순한 키-밸류 타입을 사용하고 있다. 데이터 모델을 복잡할수록 성능이 떨어지므로 redis는 단순한 구조를 통해 높은 성능을 보장한다고 할 수 있다.

NoSQL에는 다양한 제품이 있지만 이 중, redis가 주목받는 이유는 다음과 같다.

  • 데이터 저장소로 가장 입/출력이 빠른 메모리를 채택
  • 단순한 구조의 데이터 모델인 키- 밸류 방식을 통해 빠른 속도를 보장
  • 캐시 및 데이터스토어에 유리
  • 다양한 API지원

redis, memcached, 구아바 라이브러리등 인메모리 캐시방식을  적용한 제품 중 redis는 global cache 방식을 채택하였다. global cache 방식은 네트워크 트래픽이 발생하기 때문에 java heap영역에서 조회되는 local cache의 성능이 더 낫지만, WAS 인스턴스가 증가할 경우엔 캐시에 저장되는 데이터 크기가 커질수록 redis 방식이 더 유리하다.

redis는 급격한 사용자가 집중되는 상황이나 대규모의 확장이 예정되어 있는 환경에 적합하다. global cache 방식이 적용되어 was 인스턴스 확장에는 유리하지만 cache 및 redis 세션 등 관리 포인트가 늘어난다는 단점이 존재한다.

 

    Redis vs Memcached

    Redis (Remote Dictionary Storage, 레디스)와 Memcached(맴캐시드)는 유명한 오픈소스인, 인메모리 데이터 저장소이다. 둘 모두 고성능이고 쉽다. 하지만 엔진으로 사용할 때 차이를 반드시 고려야한다.  맴캐쉬드는 명료하고 단순함을 위하여 개발한 반면, 레디스는 다야한 용도에 효과적으로 사용할 수 있도록 많은 특징을 가지고 개발되었다.

      Redis Memcached
    공통점 1. 빠른 응답 시간(1ms이내)
    2. 개발의 용이성
    3. 데이터 파티셔닝 : 데이터를 노드에 분산 하여 저장가능, 수요가 증가 할 때 더 많은 데이터 효과적으로 처리하기 위하여 스케일아웃이 가능하다.
    4. 다양한 프로그래밍 언어 지원
    차이점 1. 더욱 다양한 데이터 구조 : List, Set, 정렬된 Set, Hash, Bit배열 등 다양한 자료구조 제공
    2. SnapShots : 레디스는 트겆ㅇ시점에 데이터를 저장하여 파일 보관이 가능하여 장애상황시 복구에 용이
    3. 복제 : Master -Salve 구조로, 여러개의 복제본을 만들 수 있다. 따라서 데이터베이스 읽기를 확장가능
    4. 트랜잭션 : 트랜잭션이란 데이터베이스 상태를 변경시키는 작업 단위이며, 원자성, 일관성, 독립성, 지속성의 특징을 가지고 있다. Redis는 이러한 특징을 지원한다.


    멀티스레드를 지원하기 때문에, 멀티프로세스코어를 사용할 수 있다. 따라서 스케일업을 통해 더욱 많은 작업처리를 할 수 있다.
    주의점 위의 특징만 보면, 모든 상황에서 Redis를 선택해야할 것 같습니다.
    하지만, Redis는 싱글 쓰레드이기 때문에, 1번에 1개의 명령어만 실행할 수 있습니다. Keys(저장된 모든키를 보여주는 명령어)나 flushall(모든 데이터 삭제)등의 명령어를 사용할 때, 맴캐쉬드의 경우 1ms정도 소요되지만, 레디스의 경우 100만건의 데이터 기준 1초로 엄청난 속도 차이가 있습니다.
    또한, RDB 작업(특정 간격마다 모든 데이터를 디스크에 저장)이 매우 오래걸립니다. AWS, 60기가 메모리 기준으로 10분이나 소요됩니다. Redis 장애에 원인의 대부분이 해당 기능 때문에 발생하기 때문에 사용할 때 주의해야합니다.

    단, 공통점의 스케일 아웃은  Redis에 해당된다. 즉, Memcached의 확장성은 Scale up(vertial)을 통해서 얻을 수 있는 반면, Redis는 Scale out(horizontal) 왜냐하면 레디스는 복제가 가능하며 Memcached 복제가 불가능하다. 

    Data Eviction 전략

    캐시는 유한한 리소스를 지니므로, 결국 메모리에 자리잡은 자원을 언젠가 삭제해야한다. Memcache 같은 경우 LRU(Least Recenly Used) 알고리즘만을 채택하고 있다. 반면에 Redis의 경우 보다 다양하고 미세한 방법을 제공한다.

    Redis 알고리즘 전략

    참고

    https://brownbears.tistory.com/43

     

    Memcached, Redis

    Memcached Memcached란? 무료로 사용할 수 있는 오픈소스이며 분산 메모리 캐싱 시스템 데이터 베이스의 부하를 줄여 동적 웹 어플리케이션의 속도개선을 위해 사용되기도 함 DB나 API호출 또는 렌더링

    brownbears.tistory.com

     

    'Back-end > cache' 카테고리의 다른 글

    Simple Spring Memcached(SSM) ... 작성중  (0) 2022.04.05
    [Server] Cache(캐시)  (0) 2021.12.10

    목차

      Simple Spring Memcached(SSM)

      스프링에서 Memcache를 사용하려면 SSM 라이브러리를 자주 이용한다. SSM 어노테이션으로 메서드에 선언하면 쉽게 관련 데이터가 캐시에 관리된다. 스프링에서도 3.1버전부터는 캐시 서비스 추상화 기능이 지원되어 비즈니스 로직 변경 없이 쉽게 다양한 캐시 구현체(ex. Ehcache, Redis)로 교체가 가능하게 되었다.

      Simple Spring Memcached(SSM) 설정 및 사용법

      SSM 사용에 필요한 dependency와 스프링 빈 설정 파일을 추가해야한다.

      Maven dependency 추가

      • spymemcached
        • 단순 비동기, 단일 쓰레드 memcached 자바 라이브러리
      • xmemcached (주로 사용됨)
        • 고성능 멀티 쓰레드 memcached 자바 라이브러리

      스프링 설정 파일

      더보기

      추가 설명

      ConsistentHashing 방식은 여러 서버가 변경되더라도 각 서버에 할당된 데이터를 재분배하지 않고 다운된 서버의 데이터만 다른 서버로 재분배하는 방식입니다. K(keys) = 10,000, N(memcache 서버 수 : 슬롯) = 5대 => 각 서버마다 2000 keys만큼을 hashing 할 수 있다. 서버 장애 (1 서버 다운 A)가 발생한 A 서버의 key만 다른 서버로 재분배하는 방식이다. 
      더 자세한 설명은 Consistent Hashing를 참고해주세요.

      스프링 빈 설정에 Memcached 관련 설정이 포함된다. Memcached의 서버 정보와 캐시 설정은 ConsistentHashing 방식으로 지정되어 있다.

      아래의 코드는 재직중인 회사에서 추가한 스프링 설정 파일 내용이다.

      <property name="cacheClientFactory">
          <bean class="com.google.code.ssm.providers.xmemcached.MemcacheClientFactoryImpl" />
      </property>

      SSM Cache 대표 어노테이션

      • Read
        • @ReadThroughAssignCache
        • @ReadThroughSingleCache
        • @ReadThroughMultiCache
      • Update
        • @UpdateAssignCache
        • @UpdateSingleCache
        • @UpdateMultiCache
      • Invalidate
        • @InvalidateAssignCache
        • @InvalidateSingleCache
        • @InvalidateMultiCache
      • Counter
        • @ReadCounterFromCache
        • @IncrementCounterInCache
        • @DecrementCounterInCache
      Cache Action
       Description
      ReadThrough
      Cache에 저장된 key가 없는 경우 Cache에 저장한다
      Update
      Cache에 저장된 key의 값을 업데이트한다
      Invalidate
      Cache에 저장된 key를 삭제한다
       
      Cache Type
      Description
      AssignCache
      캐시 키는 assignedKey 속성으로 지정한 값이고 메서드 인자가 없는 경우에 사용된다
      ex. List<Person> getAllUsers()와 같은 메서드에 사용된다
      SingleCache
      캐시 키는 SSM 어노테이션으로 선언된 메서드 인자로 생성되며 인자가 하나인 경우에 사용된다. 
      - 인자가 List 타입인 경우에는 캐시 키로 생성해서 사용할 수 없다
      ex. Person getUser(int int)
      MultiCache
      캐시 키는 SSM 어노테이션으로 선언된 메서드 여러 인자로 생성된다. 인자중에 한개가 List 타입 형이여야하며 반환결과도 List 타입이여야 한다. 반환된 결과 List의 각 요소는 지정된 캐시 키로 저장된다. 
      ex. List<Person> getUserFromData(List workInfo)
       
      SingleCache와 MultiCache인 경우 반드시 메서드 인자 중에 @ParameterValueKeyProvider 어노테이션을 지정해야 합니다. 기본 캐시 어노테이션 외에도 여러 어노테이션과 같이 사용하는 속성들이 존재하며 예제를 통해서 더 자세히 알아보도록 하겠습니다. 
      • 기타 어노테이션 및 속성
        • @CacheName(“QuoteApp”) :  관련 캐시를 하나로 묶을 수 있는 개념이고 클래스외에도 메서드에도 선언할 수 있다
        • @CacheKeyMethod : 캐시의 key 값으로 이용할 메서드를 선언한다
        • 캐시 key는 @CacheKeyMethod로 선언된 메서드가 사용된다
        • @CacheKeyMethod가 지정되지 않은 경우에는 Object.toString() 메서드가 사용된다
        • @ParameterValueKeyProvider : 메서드 인자에 적용되며 @CacheKeyMethod로 선언된 메서드나 toString()을 이용해서 key 값을 구한다
        • @ParameterDataUpdateContent : 메서드 인자에 어노테이션이 적용되며 새로 저장할 값을 지정한다
        • @ReturnDataUpdateContent : 반환 값을 캐시에 저장한다
      • 속성
        • namespace : 동일한 키 값의 이름이 있을 경우를 방지하기 위해서 사용된다
        • expiration : key값이 만료되는 시간 (초 단위)이다
        • assignedKey : 캐시 저장시 사용되는 키 값이다 

      Read Cache 예시

      @ReadThroughAssignCache

      @ReadThroughAssignCache 어노테이션은 인자가 없는 메서드에 적용할 수 있습니다. 캐시 영역에서 네임스페이스 'area' 이름으로 key:value(all:List<Product>)가 저장돕니다.....

      @ReadThroughAssignCache(namespace = "area", assignedKey="all")
      public List<Product> findAllProducts() {
          slowly(); // 고의로 지연시킴
          List<Product> productList = new ArrayList<>();
          return storage.values().stream().collect(Collectors.toList());
      }

      실무에서 사용

      @ReadThroughSingleCache(namespace = "intraCs", expiration = 60 * 60)
      @CacheConnectingKey("PartnerQnaCategoryCacheService.getPartnerQnaCategoryTreeList")
      public List<PartnerQnaCategoryTree> getPartnerQnaCategoryTreeList(@ParameterValueKeyProvider String serviceCode){
      ...
      }

      Unit Test

      @Test
      public void testReadThroughAssignCache() {
          this.executeWithMemcachedFlush(productService, () -> {
              productService.addProduct(new Product("microsoft", 100));
              productService.addProduct(new Product("sony", 100));
              productService.findAllProducts(); //#1 - caching
      
              productService.findAllProducts(); //#2 - cache에서 가져옴
      
          });
      }

      @ReadThroughSingleCache

      @ReadThroughSingleCache 어노테이션은 인자가 하나인 경우에만 사용한다. 인자에 @ParameterValueKeyProvider 어노테이션을 선언하면 @CacheKeyMethod로 지정된 메서드는 캐시 키를 생성하는데 사용되고 없는 경우에는 toString() 메서드가 사용됩니다.

      @ReadThroughSingleCache(namespace = "area")
      public Product findProduct(@ParameterValueKeyProvider String name) {
          slowly(); // 고의로 지연시킴
      
          return storage.get(name);
      }
      
      @CacheKeyMethod
      public String getName() {
          return name;
      }

      실무

      @ReadThroughSingleCache(namespace = "intraCs", expiration = 60 * 60)
      @CacheConnectingKey("PartnerQnaCategoryCacheService.getPartnerQnaCategoryRootPathMap")
      public Map<Long, List<PartnerQnaCategory>> getPartnerQnaCategoryRootPathMap(@ParameterValueKeyProvider String serviceCode){
      ...
      }

      Unit Test

      @Test
      public void testReadThroughSingleCache() {
          this.executeWithMemcachedFlush(productService, () -> {
              productService.addProduct(new Product("microsoft", 100));
              productService.addProduct(new Product("sony", 100));
              productService.findProduct("microsoft”); //#1 - caching
      
              productService.findProduct(("microsoft”); //#2 - cache에서 가져옴
      
          });
      }}

      @ReadThroughMultiCache

      MultiCache는 메서드 인자 중에 List 타입인 인자가 있어야 합니다. 그리고 캐시되는 key:value 인자의 요소와 반환 결과 요소가 각각 key:value로 캐시에 저장된다.

      @ReadThroughMultiCache(namespace = "area")
      public List<Integer> getIncrementValue(@ParameterValueKeyProvider List<Integer> nums, int incrementValue) {
          slowly(); // 고의로 지연시킴
          return nums.stream().map(x -> x + incrementValue).collect(Collectors.toList());
      }

      실무

      @ReadThroughMultiCache(namespace = "intraCs", expiration = 60 * 60)
      public List<OpenMarketDealInfo> getOpenMarketDealInfo(@ParameterValueKeyProvider List<String> mainDealSrlList) {
      ...
      }

      Unit Test

      @Test
      public void testReadThroughMultiCache() {
          this.executeWithMemcachedFlush(productService, () -> {
              List<Integer> nums = new ArrayList<Integer>(Arrays.asList(2, 3, 4, 5));
              productService.getIncrementValue(nums, 4); //#1 - caching 함
      
              productService.getIncrementValue(nums, 1); //#2 - caching된 값을 가져옴
      
          });
      }

      Update Cache

      Update로 시작하는 어노테이션은 캐시에 저장된 값을 강제적으로 덮어쓰는 어노테이션이다. Update에 대한 여러 어노테이션에 대해서 알아보자.

      @UpdateAssignCache

      AssignCache 어노테이션은 assignedKey로 지정된 값을 키로 사용한다.

      @ReturnDataUpdateContent
      @UpdateAssignCache(namespace = "area", assignedKey = "all")
      public List<Product> resetPriceForAllProducts() {
          slowly(); // 고의로 지연시킴
          return storage.values().stream()
                  .map(product -> {
                      product.setPrice(0);
                      return product;
                  }).collect(Collectors.toList());
      }

       

       

       

      참고

      https://advenoh.tistory.com/27#recentComments

       

      'Back-end > cache' 카테고리의 다른 글

      Redis vs Memcached  (0) 2022.04.06
      [Server] Cache(캐시)  (0) 2021.12.10

      목차

        캐시(Cache)란?

        Chche
        Chche란 자주 사용하는 데이터나 값을 미리 복사해 놓는 임시장소를 가리킨다. 아래와 같은 저장공간 계층 구조에서 확인할 수 있듯이, 캐시는 저장 공간이 작고 비용이 비싼 대신 빠른 성능을 제공한다.

        Chche를 고려하는 경우

        • 접근 시간에 비해 원래 데이터를 접근하는 시간이 오래 걸리는 경우(서버의 균일한 API 데이터)
        • 반복적으로 동일한 결과를 돌려주는 경우(이미지나 썸네일 등)
        Chche에 데이터를 미리 복사해 놓으면 계산이나 접근 시간 없이 더 빠른 속도로 데이터에 접근 가능하다. 지속적으로 DBMS 혹은 서버에 요청하는 것이 아니라 Memory에 데이터를 저장하였다가 불러다 쓰는 것을 의미한다. DBMS의 부하를 줄이고 성능을 높이기 위해 캐시(Cache)를 사용한다. 원하는 데이터가 캐시에 존재할 경우 해당 데이터를 반환하며, 이러한 상황을 Cache hit라고한다. 반대로 캐시에 원하는 데이터가 없을 시 Cache Miss라고 한다. 

        Local Cache vs Global Cache.

        프링의 PSA 덕분에, 우리는 서비스 환경에 따라 어떤 Cache 전략이던 CacheManager를 구현하고 있기만 하다면 유연하게 우리 코드에 적용할 수 있습니다.

         

        그럼 어떠한 Cache 기술을 적용할지 고려해보아야 되겠죠? Cache 관리 전략을 선택할 때 가장 먼저 고려해야 할 요소는, 캐시 데이터를 저장할 스토리지를 서버가 자체적으로 소유하고 있을지, 외부 서버에 캐시 저장소를 따로 둘 지에 대한 부분입니다. 캐시 저장소를 서버에 두는 방식을 Local Cache, 외부 캐시 저장소를 두는 방식을 Global Cache라고 합니다.

        Local Cache

        데이터 조회 오버헤드가 없다.

        캐시 데이터를 서버 메모리상에 두는 것의 가장 큰 장점은, 무엇보다도 속도가 빠르다는 점입니다. 캐시를 외부 저장소에 저장하면 네트워크 통신을 통해 캐시 저장소에 접근하고, 데이터를 가져오는 과정 등의 오버헤드가 없기 때문에 Local Cache의 데이터 읽기 속도는 현저히 빠릅니다.

        서버 간 데이터 일관성이 깨질 수 있다.

        Local Cache는 단일 서버 인스턴스에 캐시 데이터를 저장하기 때문에, 서버의 인스턴스가 여러 개일 경우 서버 간 캐시 데이터가 일치하지 않아 신뢰성을 떨어뜨릴 수 있습니다.

        서버간 동기화가 어렵고, 동기화 비용이 발생한다.

        캐시 일관성을 유지하기 위해 동기화를 한다고 하더라도, 추가적인 비용이 발생합니다. 더군다나 서버의 개수가 늘어날수록, 자신을 제외한 모든 인스턴스와 동기화 작업을 해야 하기 때문에 비용의 크기는 서버의 개수의 제곱에 비례하여 증가합니다.

        더보기

        그럼에도 불구하고 나는 왜 로컬 캐시를 썼는가?

        금칙어 조회같은 경우 CS에서 특정부분에만 국한되어 사용되며 데이터의 추가, 변경, 삭제가 빈번하지 않다. 따라서 로컬캐시로 관리하는것이 좋다고 판단하여 사용하였다. 또한 금칙어 몇개가 전체 비즈니스에 영향을 크게 주지않는다. 무슨말이냐면 글로벌 캐싱으로 얻는 이점인 서버간 데이터 일관성에 예를들면 금칙어 몇개가 빠르게 동기화되어 일관성이 유지 안된다고하여 전체 비즈니스에 영향을 미치지 않는다 따라서 로컬 캐싱을 사용하였다. 무슨 금전이 오고 가는것도 아니니

        Global Cache

        네트워크 I/O 비용 발생

        Global Cache는 외부 캐시 저장소에 접근하여 데이터를 가져오기 때문에, 이 과정에서 네트워크 I/O가 비용이 발생합니다. 하지만 서버 인스턴스가 추가될 때에도 동일한 비용만을 요구하기 때문에, 서버가 고도화될수록 더 높은 효율을 발휘합니다.

        데이터 일관성을 유지할 수 있다.

        Global Cache는 모든 서버의 인스턴스가 동일한 캐시 저장소에 접근하기 때문에, 데이터의 일관성을 보장할 수 있습니다. 데이터의 일관성이 깨지기 쉬운 분산 서버 환경에 적합한 구조입니다.

         

        Local Cache vs Global Cache, 어떤 기준으로 선택해야 할까?

        Local Cache와 Global Cache의 특성을 고려했을 때, 어떤 기술을 선택해야 할지에 대한 기준은 "데이터의 일관성이 깨져도 비즈니스에 영향을 주지 않는가?"라고 생각합니다.

         

        이를테면, 사용자 정보가 변경되어 프로필에 반영되어야 하는 상황을 가정할 때, 서버 간 동기화가 맞지 않아서 프로필에 반영되는데 시간이 조금 걸린다 하더라도, 전체적인 서비스 운영에 큰 타격을 주지는 않습니다. 금전이 오고가는 문제도 아니고, 프로필의 정보가 조금 늦게 반영된다고 해서 큰 문제가 발생하지 않으니까요. 이러한 경우에는 서버간 동기화 없이 서버 자체적으로 로컬 캐싱을 하는 것도 괜찮은 선택지라고 생각합니다.

         

        하지만, 상품 데이터를 캐싱한다고 했을 때는 상황이 달라집니다. 사용자가 가격을 변경했는데, 그것이 반영되지 않으면 서비스 신뢰를 심각하게 손상하고, 운이 나쁘면 법적 문제로 이어지기도 합니다. 따라서, 이러한 경우에는 동기화가 속도보다 더 중요하며, 그렇기에 동기화가 확실하게 보장되는 Global Cache를 사용하는 것이 좋습니다.

         

        진행 중인 프로젝트에서는 게시글과 댓글에 캐싱을 적용하였고, 둘 다 데이터의 일관성이 중요하다고 판단되어 Global Cache인 Redis를 적용하였습니다. 추후 성능 테스트를 진행하면서, Local Cache를 적용해 성능을 향상할 수 있는 지점을 발견하면, CompositeCacheManager를 사용해 2차 캐시를 구성해보고, 관련한 내용을 새로 포스팅하도록 하겠습니다.

         


        Local-Memory 캐시란?

        스프링에서 제공하는 기본 Cache (SSM)
        spring 3.1버전부터 Spring Application에 캐시를 쉽게 추가할 수 있도록 기능을 제공한다. 유사 트랜잭션을 지원하고, 사용하고 있는 코드(메서드)에 영향을 최소하하면서 일관된 방법으로 캐시를 사용 할 수 있게 된다.

        Spring에서 캐시 추상화는 메소드를 통해 기능을 지원하는데, 메소드가 실행되는 시점에 파라미터에 대한 캐시 존재 여부를 판단하여 없으면 캐시를 등록하고, 캐시가 있으면 메소드를 실행시키지 않고 캐시 데이터를 Return 해주게 된다.
        Spring 캐시 추상화를 지원하기 때문에 개발자는 캐시 로직을 작성하지 않아도 된다. 하지만 캐시를 저장하는 저장소는 직접 설정을 해줘야 한다. Spring에서는 CacheManager라는 Interface를 제공하여 캐시를 구현하도록 되어있다. 
        CacheManager 캐시 저장소
        Spring 에서는 CacheManager 라는 Interface를 제공하여 캐시를 구현하도록 하고 있다. 별다른 의존성을 추가하지 않을 시, Local-Memory에 저장이 가능한 ConcurrentMap 기반인 ConcurrentMapCacheManager가 Bean으로 자동 등록된다.

        캐시 사용하기

        캐시 설정 등록
        @EnableCaching or @Configuration 등록
        캐시 저장
        @Cachealbe을 통해 캐시할 메서드를 지정한다.
        키를 따로 설정하지 않으면 전체 파라미터가 키가 된다.

        @Cacheable

        • value, cacheNames : 캐시이름
        • key : 같은 캐시명을 사용 할 때, 구분되는 구분 값(KeyGenerator와 함께 쓸 수 없음), 별도 지정이 없을 시 파라미터로 key를 지정.
        • keyGenerator: 특정 로직에 의해 cache key를 만들고자 하는 경우 사용. 4.0이후 버전부터 SimpleKeyGenerator를 사용. Custom Key Generrator를 사용하고 싶으면, KeyGenerrator 인터페이스를 별도로 구현
        • cacheManager : 사용할 CacheManager를 지정(EHCacheManger, RadisCacheManager등)
        • cacheResolver: Cache 키에 대한 결과값을 돌려주는 Resolver (Interceptor역할).
        • CacheResolver를 구현하여 Custom하게 처리 할 수도 있음
        • unless: 캐싱이 이루어지지 않는 조건을 설정. 연산 조건이 true 이면 경우에는 캐싱되지 않음. ex) id가 null아 아닌 경우에만 캐싱 (unless = "#id == null") 
        • sync: 캐시 구현체가 Thread safe 하지 않는 경우, 자체적으로 캐시에 동기화를 거는 속성. default는 false
        • condition: SpEL 표현식을 통해 특정 조건에 부합하는 경우에만 캐시 사용. and, or 표현식등을 통해 복수 조건 사용가능. 연산 조건이 true인 경우에만 캐싱

        Global 캐시 적용 과정

        1. bulid, gradle or maven 등록
        //gradle 등록 예시
        implementation 'org.springframework.boot:spring-boot-starter-data-redis'
        2. 캐시 적용을 위한 캐시매니저 등록
        더보기

        CacheConfig 설정 과정

        @RequiredArgsConstructor
        /** @EnableCaching :해당 애플리케이션에서 캐싱을 이용하겠다는 명시를 처리해줘야 한다.
        해당 어노테이션을 적용하게 되면 @Cacheable 라는 어노테이션이 적용된 메서드가 실행될 때 마다 
        AOP의 원리인 후처리 빈에 의해 해당 메소드에 프록시가 적용되어 캐시를 적용하는 부가기능이 추가되어 작동하게 된다.
        */
        @EnableCaching
        @Configuration
        public class CacheConfig {
        
          /** 이전에 Redis를 이용한 세션 스토리지 등록시 사용했던 Lettuce 기반의 Redis client를 
          Bean으로 등록하여사용하고 있다. 
          */
            private final RedisConnectionFactory redisConnectionFactory;  
        
            /**
            CacheProperties : 캐싱이 적용되는 대상마다 캐시의 만료기간을 쉽게 변경할 수 있도록
            yml(또는 properties) 파일에서 종류별로 만료 기간을 Map에 바인딩한다.
            */
            private final CacheProperties cacheProperties; 
        
            private ObjectMapper objectMapper() {
                ObjectMapper mapper = new ObjectMapper();
                mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
                mapper.registerModule(new JavaTimeModule());
                mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
                return mapper;
            }
        
            /**
            RedisCacheConfiguration : redisCacheManager에 여러가지 옵션을 부여할 수 있는 오브젝트이다.
            여기서는 캐시의 Key/Value를 직렬화-역직렬화 하는 Pair를 설정했다.
            */
            private RedisCacheConfiguration redisCacheDefaultConfiguration() { 
                RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
                    .defaultCacheConfig()
                    .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                    .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper())));
                return redisCacheConfiguration;
            }
        
        
            /**
           CacheProperties에서 바인딩해서 가져온 캐시명과 TTL 값으로  RedisCacheConfiguration을 만들고 
           Map에 넣어 반환한다. 
           Map을 사용하는 이유는 캐시의 만료기간이 다른 여러개의 캐시매니저를 만들게 됨으로써 발생하는 
           성능저하를 방지하기 위해 하나의 캐시매니저에 Map을 이용하여 캐시 이름별 만료기간을 다르게 사용하기 위함이다.
            */
            private Map<String, RedisCacheConfiguration> redisCacheConfigurationMap() { 
                Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
                for (Entry<String, Long> cacheNameAndTimeout : cacheProperties.getTtl().entrySet()) {
                    cacheConfigurations
                        .put(cacheNameAndTimeout.getKey(), redisCacheDefaultConfiguration().entryTtl(
                            Duration.ofSeconds(cacheNameAndTimeout.getValue())));
                }
                return cacheConfigurations;
            }
        
            /**
            캐시 매니저를 등록한다.  스프링에서 기본적으로 지원하는 캐시 저장소는 JDK의 ConcuurentHashMap이며 
            그 외 캐시 저장소를 사용하기 위해서는 캐시 매니저를 Bean으로 등록해서 사용해야 한다.
        withInitialCacheConfigurations에 캐시의 종류별로 만료기간을 설정한  redisCacheConfigurationMap을 
            */ 
            @Bean
            public CacheManager redisCacheManager() {
                RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder
                    .fromConnectionFactory(redisConnectionFactory)
                    .cacheDefaults(redisCacheDefaultConfiguration())
                    .withInitialCacheConfigurations(redisCacheConfigurationMap()).build();
                return redisCacheManager;
            }
        }
        3. @Cacheable 등록
        //JPA 호출
        @Cacheable(value = "product", key = "#id")
        public ProductInfoResponse getProductInfo(Long id) {
             return productRepository.findById(id).orElseThrow(() -> new ProductNotFoundException())
                 .toProductInfoResponse();
        }

        @Cacheable 동작 원리

        • @Cacheable은 AOP 기반으로 동작하며...어쩌구
        • 쉽게 말해서 @Cacheable이 등록된 메서드는 DB에서 쿼리문을 날려 데이터를 가져오는 것이 아닌, 캐시 메모리에서 데이터를 반환하는 것이다.
        • 캐시의 value값은 필수이며 key는 선택적이다.
        • 만약 파라미터가 존재하지 않을 경우 key값은 0으로 처리된다. 왠만하면 key값을 명시적으로 사용하는 것을 권장한다.
        4. @CacheEvict
        @CacheEvict(value = "product", key = "#id")
        @Transactional
        public void updateProduct(Long id, SaveRequest updatedProduct) {
            Product savedProduct = productRepository.findById(id)
               .orElseThrow(ProductNotFoundException::new);
        
             checkDuplicateUpdatedModelNumber(savedProduct.getModelNumber(),
                 updatedProduct.getModelNumber());
        
              savedProduct.update(updatedProduct);
        }

        @CacheEvict은 지정된 key 값에 해당하는 모든 캐시를 삭제하는 어노테이션이다.

        'Back-end > cache' 카테고리의 다른 글

        Redis vs Memcached  (0) 2022.04.06
        Simple Spring Memcached(SSM) ... 작성중  (0) 2022.04.05

        + Recent posts