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을 지정해주었다.
이 세 개의 인자에 대해서 설명하자면 아래와 같다.
- value는 ehcache.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 에 다시 호출당해 로그가 찍혔다.
테스트 코드는 아래 링크를 통해 더 자세히 볼 수 있다.
참고 1
ehcache3 는 캐싱할 데이터를 외부 메모리(offheap 혹은 disk)에 저장하기 위해서는 저장할 데이터(객체 혹은 인스턴스)가 Serializable이 구현 되어 있어야 한다.
즉, 캐싱할 데이터는 Serializable을 상속받은 클래스여야 한다.
왜냐하면, ehcache가 JVM의 힙 메모리가 아닌 곳(offheap 혹은 disk)에 캐시를 저장하기 위해서는 JVM 메모리에 인스턴스화 되어있는 객체의 데이터를 외부에서 사용할 수 있게 하기 위해 Serialize(직렬화)가 필요하기 때문이다.
직렬화란?
- 자바 시스템 내부에서 사용되는 Object 또는 Data를 외부의 자바 시스템에서도 사용할 수 있도록 byte 형태로 데이터를 변환하는 기술
- JVM(Java Virtual Machine 이하 JVM)의 메모리에 상주(힙 또는 스택)되어 있는 객체 데이터를 바이트 형태로 변환하는 기술
참고 2
'개발관련' 카테고리의 다른 글
객체 지향 원리와 이해 (0) | 2021.04.20 |
---|---|
Mysql 특정 요일의 날짜 값 구하는 쿼리 작성법 (예: 저번주 토요일, 이번주 월요일) (0) | 2020.04.22 |
Jenkins JVM 힙 메모리 사이즈 수정 방법 (0) | 2020.03.25 |
JVM 모니터링 명령어 & 가비지 컬렉터 모니터링 명령어 (0) | 2020.03.04 |
Spring(Java) 함수 실행시간 측정 방법 (0) | 2020.03.04 |