-
Spring 성능 개선 - 캐시 사용하기 (Enhcache)카테고리 없음 2024. 8. 22. 21:42
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 사용량이 더 크다.