Spring 성능 개선 - 캐시 사용하기 (Enhcache)
1. Ehcache 추가
1.1 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
1.2 ehcache.xml 추가
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="0"
eternal="false"
statistics="false"
timeToIdleSeconds="10"
timeToLiveSeconds="10"
overflowToDisk="false"
diskPersistent="false"
memoryStoreEvictionPolicy="LRU"/>
<cache
name="NoticeReadMapper.findAll"
maxElementsInMemory="10000"
maxElementsOnDisk="0"
eternal="false"
statistics="false"
timeToIdleSeconds="10"
timeToLiveSeconds="10"
overflowToDisk="false"
diskPersistent="false"
memoryStoreEvictionPolicy="LRU"/>
<cache
name="NoticeReadMapper.findByPage"
maxElementsInMemory="10000"
maxElementsOnDisk="0"
eternal="false"
statistics="false"
timeToIdleSeconds="10"
timeToLiveSeconds="10"
overflowToDisk="false"
diskPersistent="false"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
- name : 캐시명
- maxElementsInMemory : 메모리에 저장할 수 있는 최대 요소 수
- maxElementsOnDisk : 디스크에 저장할 수 있는 최대 요소 수
- eternal : 캐시 항목이 영원히 유지되는지 여부 -> false로 설정되어 있긴하다만 캐시 항목은 유효기간(timeToLiveSeconds) 혹은 유휴기간(timeToIdleSeconds)이 지나면 제거된다.
- statistics : JMX 통계정보 갱신 옵션
- timeToIdleSeconds: 설정된 시간 동안 유휴상태시 갱신 -> 캐시된 데이터가 사용 되지 않은 채로 유지되는 최대 시간
- timeToLiveSeconds: 설정된 시간 동안 유지 후 갱신 -> 캐시 된 데이터의 전체 수명
- overflowToDisk : 메모리에 캐시된 데이터가 메모리 한계를 초과하는 경우 디스크로 넘길지 여부 지정
- diskPersistent: 디스크에 저장된 데이터가 시스템 재시작 후에도 유지되어야 하는지 여부
- memoryStoreEvictionPolicy: 메모리가 꽉 찼을 때 데이터 제거 알고리즘 옵션
1.3 ehcache Configuration 추가
EhCache를 사용할 수 있도록 EhCacheManagerFactoryBean과 EhCacheCacheManager를 Bean으로 등록
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
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.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
@Configuration
@EnableCaching
public class EhcacheConfiguration {
@Bean
@Primary
public CacheManager cacheManager(EhCacheManagerFactoryBean ehCacheManagerFactoryBean) {
return new EhCacheCacheManager(ehCacheManagerFactoryBean.getObject());
}
@Bean
public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();
ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
ehCacheManagerFactoryBean.setShared(true);
return ehCacheManagerFactoryBean;
}
}
2. 캐시 사용하기
@Cacheable 사용하기 : value속성은 캐시의 이름, ehcache.xml 구성 파일에서 <cache>엘리먼트의 name과 일치시키도록 하자
@Override
@Cacheable(value = "NoticeReadMapper.findAll")
@Transactional
public List<Notice> getAllNotices() {
return noticeReadMapper.findAll();
}
@Cacheable 옵션 활용
@Override
@Cacheable(value = "NoticeReadMapper.findByPage", key = "#request.requestURI + '-' + #pageNumber", condition = "#pageNumber <= 5")
public List<Notice> findByPage(HttpServletRequest request, int pageNumber) {
int startIdx = (pageNumber - 1) * 10;
return noticeReadMapper.findByPage(startIdx);
}
NoticeReadMapper.findByPage" 로 캐시 이름은 동일한데 1page에 대한 캐시인지 2page에 대한 캐시인지 어떻게 구분할까
- 캐시의 키를 동적으로 생성하기 위한 SpEL식을 지정한다.
- 메소드의 파라미터를 이용하여 특정 파라미터 값을 기반으로 캐시 키를 생성할 수 있다.
- condition속성은 캐시가 적용되기 위한 추가적인 조건을 지정할 때 사용한다. -> true인 경우에만 캐시적
- 여기선 pageNumber가 5이하인 경우에만 캐싱처리
2. nGrinder로 캐시 전후 성능테스트 1 - 요청당 5000건 조회
- 요청 당 5000건에 공지사항 전체 데이터를 조회
- uri : get /api/notices
- 캐시 적용 전
- Vuser:10
- Duration: 1분
- 캐시 적용 후
- Vuser:10
- TTL 20초
- Duration 1분
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="0"
eternal="false"
statistics="false"
timeToIdleSeconds="20"
timeToLiveSeconds="20"
overflowToDisk="false"
diskPersistent="false"
memoryStoreEvictionPolicy="LRU"/>
<cache //이하생략
- TTL을 20초로 설정했기 때문에 20초로 수정했다. (원래 10초였다)
2.1 캐싱 적용 전
먼저 캐시 어노테이션에 주석 처리
@Override
//@Cacheable(value = "NoticeReadMapper.findAll")
public List<Notice> getAllNotices() {
return noticeReadMapper.findAll();
}
다음으로 스크립트를 셋팅할 것이다.
아래와 같이 스크립트와 테스트를 셋팅한다.
1분 테스트 결과 아래와 같은 정보를 얻을 수 있다.
환경에 따라 테스트 결과가 달라질 수 있지만, 꽤 무거운 API이므로, 그리 높은 TPS수치를 보여주진않는다.
이제 캐시를 적용한 후 동일한 테스트를 진행해보자
2.2 캐시 적용 후
캐시 적용 후 TPS
한 눈에 봐도 TPS 수치가 많이 좋아진 것을 확인할 수 있다.
3. nGrinder로 캐시 전후 성능테스트 1 - 1~10 page 랜덤 조회
--> page에 대한 요청을 랜덤으로 보내기 위해 스크립트에 api 요청을 살짝 수정했다.
@Test
public void test() {
def page = new Random().nextInt(10)+1
def apiUrl = "http://127.0.0.1:8081/api/notices/${page}"
HTTPResponse response = request.GET(apiUrl, params)
if (response.statusCode == 301 || response.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
} else {
assertThat(response.statusCode, is(200))
}
}
- 성능 테스트 환경
- uri : get /api/notices/{page}
- 캐시 적용 전
- Vuser:10
- Duration: 1분
- 캐시 적용 후
- Vuser:10
- TTL 20초
- Duration 1분
3.1 캐시 적용 전 성능
- 요청당 10건의 데이터만 가져오므로, 꽤 준수한 TPS 수치를 보여준다.
3.2 캐시적용 후
- 캐싱은 최신 데이터를 위주로 보는 특성을 고려하여 5page까지만 적용했다.
평균 TPS : 980 -> 1326 대략 35% 증가
Peek TPS : 1302 - > 1930 대략 48% 증가
Mean Test Time : 9 -> 6 대략 33%감소
Exected Tests: 53,296 -> 74,966 대략 40% 증가
4. 모니터링 툴을 통한 수치 변화 - 공지사항 전체
- 캐시 사용 전
X로그 (맨 오른쪽), TPS를 보면 3초 이내에 응답을 하고있음을 볼 수 있다.
Heap Used -> 증가했다, 줄어들었다를 반복 -> 이는 자바의 GC가 조회한 데이터를 응답하고 사용하지않는 객체를 free하기 때문 (GC count 탭을 추가하면 확인 가능)
*실무에서 모니터링 툴 확인 팁
CPU -> 60% ~40% 이하 유지 [CPU 사용량이 꾸준하게 80~90%라면 위험할 수 있다]
XLog를 통해 10초 혹은 그 이상 걸리는 트랜잭션이 있으면, 이유를 찾는다 -> 스카우터를 통해 어느정도 확인 가능
- 캐시 사용 후
캐시 적용 후 에는 0.2초 이내에 빠르게 응답함을 볼 수 있다.
GC는 이전에 비해 더 많이 발생하게 된다.
또한 1분동안 더 많은 응답을 처리하므로 CPU 사용량이 더 크다.