아무거나

[SpringBoot] Ehcache를 사용한 Cache 사용 본문

Java & Kotlin/Spring

[SpringBoot] Ehcache를 사용한 Cache 사용

전봉근 2021. 4. 22. 00:18
반응형

Ehcache

  • build.gradle
    ...
    dependencies {
      ...
      // 캐시관련 설정을 편리하게 지원해주는 패키지 (CacheManager, EhCacheManagerFactoryBean 등의 bean 생성을 직접 안할수 있음)
      implementation 'org.springframework.boot:spring-boot-starter-cache'
    
      // Ehcache
      implementation group: 'net.sf.ehcache', name: 'ehcache', version: '2.10.6'
    }
    ...
    
  • EhCache 설정 체크 관련 Component 추가 (CommandLineRunner를 통해 Application 실행시 무조건 run() 이 실행되도록 하여 CacheManager를 확인할 수 있다.)
    • CacheManagerCheck.java
      import lombok.RequiredArgsConstructor;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.boot.CommandLineRunner;
      import org.springframework.cache.CacheManager;
      import org.springframework.stereotype.Component;
      
      @Slf4j
      @Component
      @RequiredArgsConstructor
      public class CacheManagerCheck implements CommandLineRunner {
      
          private final CacheManager cacheManager;
      
          @Override
          public void run(String... strings) {
              log.info("\n\n" + "=========================================================\n"
                      + "Using Cache Manager: " + this.cacheManager.getClass().getName() + "\n"
                      + "=========================================================\n\n");
          }
      
      }
      
    • ehcache.xml 생성 (path: /src/main/resources/ehcache.xml)
      <?xml version="1.0" encoding="UTF-8"?>
      <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
              updateCheck="false">
          <diskStore path="java.io.tmpdir" />
      
          <!--
              maxEntriesLocalHeap: 메모리에 생성될 객체의 최대 수(0: 제한없음)
              maxEntriesLocalDisk: 디스크(DiskStore)에 저장될 객체의 최대 수(0: 제한없음)
              eternal: 저장된 캐시를 제거할지 여부를 설정한다. true 인 경우 저장된 캐시는 제거되지 않으며 timeToIdleSeconds, timeToLiveSeconds 설정은 무시된다.
              └timeToIdleSeconds: 생성후 해당 시간 동안 캐쉬가 사용되지 않으면 삭제된다. 0은 삭제되지 않는 다. 단 eternal=false 인 경우에만 유효
              └timeToLiveSeconds: 생성후 해당 시간이 지나면 캐쉬는 삭제된다. 0은 삭제되지 않는 다. 단 eternal=false 인 경우에만 유효
              diskSpoolBufferSizeMB: 스풀버퍼에 대한 디스크(DiskStore) 크기 설정한다. (OutOfMemory 에러가 발생 시 설정한 크기를 낮추는 것이 좋다)
              memoryStoreEvictionPolicy: maxEntriesLocalHeap 설정 값에 도달했을때 설정된 정책에 따리 객체가 제거되고 새로 추가된다.
              └LRU: 사용이 가장 적었던 것부터 제거한다.
              └FIFO: 먼저 입력된 것부터 제거한다.
              └LFU: 사용량이 적은 것부터 제거한다.
              transactionalMode: copyOnRead, copyOnWrite 시 트랜잭션 모드를 설정 (copyOnRead, copyOnWrite 는 캐시로 받아온 객체에 수정이 일어나는 경우에 사용 -> 캐시된 객체에 수정이 일어나면 참조호출로 인하여 그 뒤에 호출되는 모든 객체가 수정영향이 중첩되어 발생하므로 주의 필요)
          -->
          <cache name="exampleChche"
                maxEntriesLocalHeap="10000"
                maxEntriesLocalDisk="1000"
                eternal="false"
                timeToIdleSeconds="300" timeToLiveSeconds="600"
                diskSpoolBufferSizeMB="20"
                memoryStoreEvictionPolicy="LFU"
                transactionalMode="off">
              <persistence strategy="localTempSwap" />
          </cache>
      </ehcache>
      
    • ApiApplication.java에 @EnableCaching 추가
      // @EnableCaching 은 이 프로젝트에서 캐시 관련 어노테이션 (@Cacheable, @CacheEvict)을 사용하겠다는 선언
      @SpringBootApplication
      @EnableCaching
      @MapperScan("com.example.bkjeon.mapper")
      public class ApiApplication {
      
          public static void main(String[] args) {
              SpringApplication.run(ApiApplication.class, args);
          }
      
      }
      
    • 테스트 클래스 작성 [CacheController.java]
      package com.example.bkjeon.base.services.api.v1.cache;
      
      import com.example.bkjeon.model.ApiResponseMessage;
      import io.swagger.annotations.ApiOperation;
      import io.swagger.annotations.ApiParam;
      import lombok.RequiredArgsConstructor;
      import org.springframework.web.bind.annotation.*;
      
      @RestController
      @RequiredArgsConstructor
      @RequestMapping("v1/cache")
      public class CacheController {
      
          private final CacheService cacheService;
      
          @ApiOperation("Cache Example Data List")
          @GetMapping("examples/cache")
          public ApiResponseMessage getCacheExampleList(
              @ApiParam(
                  value = "bkjeon: bkjeon관련 데이터, example:example 관련 데이터",
                  name = "exampleType",
                  required = true
              ) @RequestParam String exampleType
          ) {
              return cacheService.getCacheExampleList(exampleType);
          }
      
          @ApiOperation("NoCache Example Data List")
          @GetMapping("examples/noCache")
          public ApiResponseMessage getNoCacheExampleList(
              @ApiParam(
                  value = "bkjeon: bkjeon관련 데이터, example:example 관련 데이터",
                  name = "exampleType",
                  required = true
              ) @RequestParam String exampleType
          ) {
              return cacheService.getNoCacheExampleList(exampleType);
          }
      
          @ApiOperation("Clear Cache Example Data List")
          @GetMapping("examples/clearCache")
          public ApiResponseMessage getClearCacheExampleList(
              @ApiParam(
                  value = "bkjeon: bkjeon관련 데이터, example:example 관련 데이터",
                  name = "exampleType",
                  required = true
              ) @RequestParam String exampleType
          ) {
              return cacheService.getClearCacheExampleList(exampleType);
          }
      
      }
      
      [CacheService.java]
      package com.example.bkjeon.base.services.api.v1.cache;
      
      import com.example.bkjeon.entity.cache.CacheExampleData;
      import com.example.bkjeon.enums.ResponseResult;
      import com.example.bkjeon.model.ApiResponseMessage;
      import com.example.bkjeon.util.ThreadUtil;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.cache.annotation.CacheEvict;
      import org.springframework.cache.annotation.Cacheable;
      import org.springframework.stereotype.Service;
      
      import java.util.ArrayList;
      import java.util.List;
      
      @Slf4j
      @Service
      public class CacheService {
      
          public ApiResponseMessage getNoCacheExampleList(String exampleType) {
              ApiResponseMessage result = new ApiResponseMessage(
                  ResponseResult.SUCCESS,
                  "Cache 조회가 완료되었습니다."
              );
      
              try {
                  result.setContents(getExampleList(exampleType));
                  ThreadUtil.threadSleep(2000);
              } catch (Exception e) {
                  throw new IllegalStateException(e);
              }
      
              return result;
          }
      
          @Cacheable(value = "exampleCache", key = "#exampleType")
          public ApiResponseMessage getCacheExampleList(String exampleType) {
              ApiResponseMessage result = new ApiResponseMessage(
                  ResponseResult.SUCCESS,
                  "Cache 조회가 완료되었습니다."
              );
      
              try {
                  result.setContents(getExampleList(exampleType));
                  ThreadUtil.threadSleep(2000);
              } catch (Exception e) {
                  throw new IllegalStateException(e);
              }
      
              return result;
          }
      
          @CacheEvict(value = "exampleCache", key = "#exampleType")
          public ApiResponseMessage getClearCacheExampleList(String exampleType) {
              ApiResponseMessage result = new ApiResponseMessage(
                  ResponseResult.SUCCESS,
                  "NoCache 조회가 완료되었습니다."
              );
      
              try {
                  result.setContents(getExampleList(exampleType));
                  ThreadUtil.threadSleep(2000);
              } catch (Exception e) {
                  throw new IllegalStateException(e);
              }
      
              return result;
          }
      
          public List<CacheExampleData> getExampleList(String exampleType) {
              List<CacheExampleData> exampleList = new ArrayList<>();
      
              for (int i=1; i<7; i++) {
                  CacheExampleData cacheExampleData = CacheExampleData.builder()
                          .exampleNo(i)
                          .writer(exampleType)
                          .build();
                  exampleList.add(cacheExampleData);
              }
      
              return exampleList;
          }
      
      }
      
      [CacheExampleData.java]
      package com.example.bkjeon.entity.cache;
      
      import lombok.AllArgsConstructor;
      import lombok.Builder;
      import lombok.Getter;
      import lombok.NoArgsConstructor;
      
      @Builder
      @Getter
      @NoArgsConstructor
      @AllArgsConstructor
      public class CacheExampleData {
      
          private Integer exampleNo;
          private String writer;
      
      }
      
    • 테스트
      • http://localhost:9090/v1/cache/examples/cache?exampleType=bkjeon 로 연속으로 호출하면 처음 호출시에만 2초 sleep이 실행되고 그 후는 캐시를 호출하므로 바로바로 호출된다.
      • http://localhost:9090/v1/cache/examples/noCache?exampleType=bkjeon 는 cache를 사용하지 않으므로 계속 2초 sleep으로 호출된다.
      • http://localhost:9090/v1/cache/examples/cache?exampleType=example 로 하게되면 캐시로 지정한 매개변수인 exampleType 값이 다르므로 처음 호출시 2초 sleep발생 그 후 바로바로 호출됨
      • http://localhost:9090/v1/cache/examples/clearCache?exampleType=bkjeon 로 호출하여 Cache를 초기화후 다시 http://localhost:9090/v1/cache/examples/cache?exampleType=bkjeon 를 호출하면 2초 sleep후 호출된다

 

[에러케이스]

  // 테스트 코드 동작 과정에서 Another CacheManager with same name 'cacheManager' already exists in the same VM. Please 
provide unique names for each CacheManager in the config or do one of following 관련 에러     
  Another CacheManager with same name 'cacheManager' already exists in the same VM. Please 
  provide unique names for each CacheManager in the config or do one of following:
  1. Use one of the CacheManager.create() static factory methods to reuse same
    CacheManager with same name or create one if necessary
  2. Shutdown the earlier cacheManager before creating new one with same name.

  The source of the existing CacheManager is: 
  DefaultConfigurationSource [ ehcache.xml or ehcache-failsafe.xml ]

상기 관련 에러일 경우 ehcache 2.5 이상에서는 동일한 이름을 가진 여러 CacheManager 가 동일한 JVM 에 존재하는 것을 허용하지 않으며, 싱글톤이 아닌 CacheManager 를 생성하는 CacheManager() 생성자는 이 규칙을 위반할 수 있다.

 

해결방법 예시코드 (싱글톤으로 생성해주자)

import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

@Configuration
public class EhCacheConfiguration {

    @Bean
    public EhCacheCacheManager ehCacheCacheManager() {
        return new EhCacheCacheManager(ehCacheManagerFactoryBean().getObject());
    }


    @Bean
    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
        EhCacheManagerFactoryBean cacheManagerFactoryBean = new EhCacheManagerFactoryBean();
        cacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml")); 
        cacheManagerFactoryBean.setShared(true);
        return cacheManagerFactoryBean;
    }
 
}

 

반응형
Comments