본문 바로가기

개발관련

Spring 로컬 캐시 라이브러리 ehcache

반응형

Overview

Spring에서 주로 사용되는 로컬 캐시인 ehcache에 대해서 스터디한다.


ehcache는 Spring에서 간단하게 사용할 수 있는 Java기반 오픈소스 캐시 라이브러리이다.

 

redis나 memcached같은 캐시 엔진들도 있지만, 저 2개의 캐시 엔진과는 달리 ehcache는 데몬을 가지지 않고 Spring 내부적으로 동작하여 캐싱 처리를 한다.

따라서 redis같이 별도의 서버를 사용하여 생길 수 있는 네트워크 지연 혹은 단절같은 이슈에서 자유롭고 같은 로컬 환경 일지라도 별도로 구동하는 memcached와는 다르게 ehcache는 서버 어플리케이션과 라이프사이클을 같이 하므로 사용하기 더욱 간편하다.

 

ehcache는 2.x 버젼과 3 버젼의 차이가 크다.

3 버젼 부터는 javax.cache API (JSR-107)와의 호환성을 제공한다. 따라서 표준을 기반으로 만들어졌다고 볼 수 있다.

또한 기존 2.x 버젼과는 달리 3 버젼에서는 offheap 이라는 저장 공간을 제공한다. offheap이란 말 그대로 힙 메모리를 벗어난 메모리로 Java GC에 의해 데이터가 정리되지 않는 공간이다.


사용법 - 설정

해당 문서에서는 ehcache 2.x 버젼이 아닌 ehcache 3 버젼을 다룬다.

 

먼저 github에서 직접 jar를 다운로드하여 import하거나,

Maven central에서 dependency를 가져올 수 있다.

 

Maven dependency로 구성하는 방법을 알아보자.

먼저 ehcache를 사용하기 위해 아래와 같이 Maven dependency를 추가한다.

<dependency>
	<groupId>org.ehcache</groupId>
	<artifactId>ehcache</artifactId>
	<version>3.6.2</version>
</dependency>

 

 

다음으로 JSR-107 API를 사용하기 위해서 아래의 Maven dependency도 추가해야한다.

<dependency>
	<groupId>javax.cache</groupId>
	<artifactId>cache-api</artifactId>
	<version>1.1.0</version>
</dependency>

 

 

마지막으로 spring boot를 사용하고 있다면 아래의 Maven dependency를 추가한다.

버젼은 자신의 프로젝트 버젼에 알맞게 세팅한다.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-cache</artifactId>
	<version>2.1.1.RELEASE</version>
</dependency>

 

 

다음에는 캐시에 대해서 어떻게 처리할 것인지 정의하기 위해 ehcache.xml 파일을 작성해야한다.

이 파일의 위치는 프로젝트 내 resources 디렉터리 하위에 위치하도록 한다.

 

ehcache.xml은 아래와 같이 작성한다.

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.ehcache.org/v3"
        xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
        xsi:schemaLocation="
            http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
            http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">
<!-- config : XML 구성의 루트 요소이다. --> 

    <cache alias="squareCache"> <!-- cache 요소는 CachceManager에 의해 작성되고 관리될 Cache 인스턴스를 나타낸다. Cache<k,v> 형태로 인스턴스가 생성된다. alias에는 캐시의 이름을 지정한다. -->
        <key-type>java.lang.Long</key-type> <!-- key-type 요소는 Cache 인스턴스에 저장될 캐시의 키의 FQCN을 지정한다. 즉, 키의 타입을 명시해주면 된다. 기본 값은 java.lang.Object 이다. -->
        <value-type>java.math.BigDecimal</value-type> <!-- value-type 요소는 Cache 인스턴스에 저장된 값의 FQCN을 지정한다. 기본 값은 java.lang.Object 이다. -->
        <expiry> <!-- expiry는 캐시 만료기간에 대해 설정하는 요소이다. -->
            <ttl unit="seconds">30</ttl> <!-- ttl에는 캐시 만료 시간을 지정하며 unit에는 단위를 지정한다. 해당 요소는 30초 뒤 캐시가 만료되는 것으로 지정되어 있다. -->
            <!-- unit은 days, hours, minutes, seconds, millis, micros, nanos 를 세팅할 수 있다. -->
        </expiry>

        <listeners> <!-- listeners는 Cache의 리스너를 등록하는 요소이다. -->
            <listener>
                <class>com.example.ehcache.demo.config.CacheEventLogger</class> <!-- 캐싱처리에 대한 리스너가 등록된 클래스의 FQCN을 등록한다. -->
                <event-firing-mode>ASYNCHRONOUS</event-firing-mode>
                <event-ordering-mode>UNORDERED</event-ordering-mode>
                <events-to-fire-on>CREATED</events-to-fire-on>
                <events-to-fire-on>EXPIRED</events-to-fire-on>
            </listener>
        </listeners>

        <resources> <!-- resources는 캐시 데이터의 저장 공간과 용량을 지정한다. 만약 힙 메모리만 사용한다면 <heap> 요소만으로 대체할 수 있다.  -->
            <heap unit="entries">2</heap> <!-- heap은 JVM 힙 메모리에 캐시를 저장하도록 세팅하는 요소이다. -->
            <offheap unit="MB">10</offheap> <!-- offheap은 JVM 힙 메모리 외부의 메모리에 캐시를 저장하도록 세팅하는 요소이다. -->
        </resources>
    </cache>

    <cache alias="taskCache">
        <key-type>org.springframework.cache.interceptor.SimpleKey</key-type> <!-- 만약에 캐시의 키로 지정할 것이 없다면 key-type를 이렇게 세팅하고 캐싱할 데이터를 Serialize(정규화)한다. -->
        <value-type>java.util.List</value-type>
        <expiry>
            <ttl unit="seconds">30</ttl>
        </expiry>

        <listeners>
            <listener>
                <class>com.example.ehcache.demo.config.CacheEventLogger</class>
                <event-firing-mode>ASYNCHRONOUS</event-firing-mode>
                <event-ordering-mode>UNORDERED</event-ordering-mode>
                <events-to-fire-on>CREATED</events-to-fire-on>
                <events-to-fire-on>EXPIRED</events-to-fire-on>
            </listener>
        </listeners>

        <resources>
            <heap unit="entries">2</heap>
            <offheap unit="MB">10</offheap>
        </resources>
    </cache>

    <cache alias="taskByUserIdCache">
        <key-type>java.lang.Integer</key-type>
        <value-type>java.util.List</value-type>
        <expiry>
            <ttl unit="seconds">30</ttl>
        </expiry>

        <listeners>
            <listener>
                <class>com.example.ehcache.demo.config.CacheEventLogger</class>
                <event-firing-mode>ASYNCHRONOUS</event-firing-mode>
                <event-ordering-mode>UNORDERED</event-ordering-mode>
                <events-to-fire-on>CREATED</events-to-fire-on>
                <events-to-fire-on>EXPIRED</events-to-fire-on>
            </listener>
        </listeners>

        <resources>
            <heap unit="entries">2</heap>
            <offheap unit="MB">10</offheap>
        </resources>
    </cache>
</config>

 

그리고 작성한 ehcache.xml 파일을 Spring이 알도록 하기 위해 properties에 아래 텍스트를 추가한다.

spring.cache.jcache.config=classpath:ehcache.xml

 

마지막으로 메인 클래스에 @EnableCaching 어노테이션을 붙여주면 ehcache를 사용하기 위한 설정은 끝난다.

@EnableCaching
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

 


사용법 - 실제 사용

우선 캐싱 처리에 대해서 모니터링을 위하여 CacheEventLogger 라는 클래스를 만들자.

@Slf4j
public class CacheEventLogger implements CacheEventListener<Object, Object> {

    public void onEvent(CacheEvent<? extends Object, ? extends Object> cacheEvent) {
        log.info("cache event logger message. getKey: {} / getOldValue: {} / getNewValue:{}", cacheEvent.getKey(), cacheEvent.getOldValue(), cacheEvent.getNewValue());
    }
}

 

이제 캐싱 처리가 되면 이 CacheEventLogger 클래스 내부의 onEvent() 메서드가 호출되면서 로그가 찍힐 것이다.

다음으로는 캐싱 테스트를 위해 아래와 같이 컨트롤러 코드를 작성한다.

@Slf4j
@RestController
@RequestMapping("/number")
public class NumberController {

    @Autowired
    NumberService numberService;

    @GetMapping(path = "/square/{number}")
    public String getSquare(@PathVariable Long number) {
        log.info("call numberService to square {}", number);
        return String.format("{\"square\": %s}", numberService.square(number));
    }
}

 

 

해당 컨트롤러를 통해 NumberService.square() 메서드를 호출하게 된다.

NumberService.square()에서 실질적인 데이터를 리턴 해줄텐데, 여기에서 캐싱 처리를 하여 테스트를 해보도록 한다.

그럼 NumberService를 아래와 같이 작성해보자.

@Slf4j
@Service
public class NumberService {

    @Cacheable(value = "squareCache", key = "#number", condition = "#number > 10")
    public BigDecimal square(Long number) {
        BigDecimal square = BigDecimal.valueOf(number).multiply(BigDecimal.valueOf(number));
        log.info("square of {} is {}", number, square);
        return square;
    }

}

 

 

@Cacheable 어노테이션은 캐시하려는 메서드를 지정하는데 사용한다.

즉, 이 어노테이션을 설정한 메서드는 결과를 캐시에 저장하고, 뒤이은 호출에는 실제로 메서드를 실행하지 않고 캐시에 저장된 값을 반환한다.

@Cacheable의 인자로는 value, key, condition을 지정해주었다.

이 세 개의 인자에 대해서 설명하자면 아래와 같다.

  • valueehcache.xml에서 등록했던 캐시 중 메서드에 적용할 캐시의 이름(ehcache.xml의 cache 요소에 등록했던 alias 값)을 등록한다.
  • key는 캐시를 구분하기 위한 용도로 사용된다. 만약 캐시를 구분할 필요가 없다면 key를 등록하지 않는다.
  • condition은 캐시 처리에 대한 조건을 지정한다. 위에서는 condition을 #number > 10으로 지정 했으므로 이를 해석하면 매개변수 Long number가 10보다 큰 경우에만 캐시 처리를 한다는 뜻이다.

이 세가지 인자 외에도 다양한 인자를 넣을 수 있다.(cacheName, keyGenerator, cacheManager, cacheResolver, unless, sync)

이제 테스트를 위한 코드는 모두 작성했으니 실행하고 API를 호출하여 테스트해보자.

 

2020-03-26 14:57:24.103  INFO 3327 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-03-26 14:57:24.103  INFO 3327 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2020-03-26 14:57:24.108  INFO 3327 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 5 ms
2020-03-26 14:57:24.132  INFO 3327 --- [nio-8080-exec-1] c.e.e.demo.controller.NumberController   : call numberService to square 35
2020-03-26 14:57:24.154  INFO 3327 --- [nio-8080-exec-1] c.e.ehcache.demo.service.NumberService   : square of 35 is 1225
2020-03-26 14:57:24.164  INFO 3327 --- [e [_default_]-0] c.e.e.demo.config.CacheEventLogger       : cache event logger message. getKey: 35 / getOldValue: null / getNewValue:1225
2020-03-26 14:57:28.027  INFO 3327 --- [nio-8080-exec-3] c.e.e.demo.controller.NumberController   : call numberService to square 35
2020-03-26 14:57:28.634  INFO 3327 --- [nio-8080-exec-4] c.e.e.demo.controller.NumberController   : call numberService to square 35
2020-03-26 14:57:29.087  INFO 3327 --- [nio-8080-exec-5] c.e.e.demo.controller.NumberController   : call numberService to square 35
2020-03-26 14:57:29.543  INFO 3327 --- [nio-8080-exec-6] c.e.e.demo.controller.NumberController   : call numberService to square 35
2020-03-26 14:57:29.974  INFO 3327 --- [nio-8080-exec-7] c.e.e.demo.controller.NumberController   : call numberService to square 35
2020-03-26 14:57:30.459  INFO 3327 --- [nio-8080-exec-8] c.e.e.demo.controller.NumberController   : call numberService to square 35
2020-03-26 14:57:30.956  INFO 3327 --- [nio-8080-exec-9] c.e.e.demo.controller.NumberController   : call numberService to square 35
2020-03-26 14:57:51.221  INFO 3327 --- [io-8080-exec-10] c.e.e.demo.controller.NumberController   : call numberService to square 35
2020-03-26 14:57:56.898  INFO 3327 --- [nio-8080-exec-2] c.e.e.demo.controller.NumberController   : call numberService to square 35
2020-03-26 14:57:56.901  INFO 3327 --- [e [_default_]-1] c.e.e.demo.config.CacheEventLogger       : cache event logger message. getKey: 35 / getOldValue: 1225 / getNewValue:null
2020-03-26 14:57:56.901  INFO 3327 --- [nio-8080-exec-2] c.e.ehcache.demo.service.NumberService   : square of 35 is 1225
2020-03-26 14:57:56.901  INFO 3327 --- [e [_default_]-1] c.e.e.demo.config.CacheEventLogger       : cache event logger message. getKey: 35 / getOldValue: null / getNewValue:1225
2020-03-26 14:57:59.600  INFO 3327 --- [nio-8080-exec-1] c.e.e.demo.controller.NumberController   : call numberService to square 35
2020-03-26 14:58:00.266  INFO 3327 --- [nio-8080-exec-3] c.e.e.demo.controller.NumberController   : call numberService to square 35

 

위와 같이 NumberService가 30초 동안에는 캐싱되어 NumberController에서 호출 해도 실행되지 않고 바로 결과 값을 리턴 해주는 것을 확인할 수 있다.

이는 NumberService의 로그가 호출당하고 난 뒤의 30초 동안은 찍히지 않는 것으로 판단할 수 있다.

14:57:24.154 에 호출 당한 이후로 30초 동안은 로그가 찍히지 않았고, 그 뒤 30초가 지난 후 14:57:56.901 에 다시 호출당해 로그가 찍혔다.

 

테스트 코드는 아래 링크를 통해 더 자세히 볼 수 있다.

 

BangShinChul/Spring-boot-ehcache-demo-project

Spring boot의 ehcache 기능을 테스트하기 위한 데모 프로젝트 입니다. Contribute to BangShinChul/Spring-boot-ehcache-demo-project development by creating an account on GitHub.

github.com

 

 


참고 1

ehcache3 는 캐싱할 데이터를 외부 메모리(offheap 혹은 disk)에 저장하기 위해서는 저장할 데이터(객체 혹은 인스턴스)가 Serializable이 구현 되어 있어야 한다.

즉, 캐싱할 데이터는 Serializable을 상속받은 클래스여야 한다.

왜냐하면, ehcache가 JVM의 힙 메모리가 아닌 곳(offheap 혹은 disk)에 캐시를 저장하기 위해서는 JVM 메모리에 인스턴스화 되어있는 객체의 데이터를 외부에서 사용할 수 있게 하기 위해 Serialize(직렬화)가 필요하기 때문이다.

 

직렬화란?

  • 자바 시스템 내부에서 사용되는 Object 또는 Data를 외부의 자바 시스템에서도 사용할 수 있도록 byte 형태로 데이터를 변환하는 기술
  • JVM(Java Virtual Machine 이하 JVM)의 메모리에 상주(힙 또는 스택)되어 있는 객체 데이터를 바이트 형태로 변환하는 기술

 

참고 2

 

 

ehcache value need to implement serializable?

For Ehcache, are there any constraints on the value that needs to be put in to? Like key does the value also need to implement 'serializable'

stackoverflow.com

 

 

Serializers and Copiers

As the on-heap store is capable of storing plain java objects as such, it is not necessary to rely on a serialization mechanism to copy keys and values in order to provide by value semantics. Other forms of copy mechanism can be a lot more performant, such

www.ehcache.org

 

반응형