카테고리 없음

Spring 성능 개선 - 캐시 사용하기 (Enhcache)

now0204 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 사용량이 더 크다.