본문 바로가기
개발/성능개선

Ehcache

by BellOne4222 2024. 3. 8.

ehcache.xml 설정

  • name : 캐시의 이름
  • maxElementsInMemory : 메모리에 저장할 수 있는 최대 요소 수
  • maxElementsOnDisk : 디스크에 저장할 수 있는 최대 요소 수
  • eternal : 캐시 항목이 영원히 유지되는지 여부
    • false로 설정하면 캐시 항목은 유효 기간(timeToLiveSeconds) 또는 유휴 기간(timeToIdleSeconds)이 지나면 제거
  • statistics : JMX 통계정보 갱신 옵션
  • timeToIdleSeconds
    • 설정된 시간 동안 유지 후 갱신
    • 캐시 된 데이터의 전체 수명
  • overflowToDisk : 메모리에 캐시된 데이터가 메모리 한계를 초과하는 경우 디스크로 넘길지 여부를 지정
  • diskPersistent : 디스크에 저장된 데이터가 시스템 재시작 후에도 유지되어야 하는지 여부
  • memoryStoreEvictionPolicy : 메모리가 꽉 찼을 때 데이터 제거 알고리즘 옵션
    • LRU, LFU, FIFO
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>

    <defaultCache
            maxElementsInMemory="1000"
            maxElementsOnDisk="0"
            eternal="false"
            statistics="false"
            timeToIdleSeconds="60"
            timeToLiveSeconds="60"
            overflowToDisk="false"
            diskPersistent="false"
            memoryStoreEvictionPolicy="LRU"/>

    <cache
            name="NoticeReadMapper.findAll"
            maxElementsInMemory="10000"
            maxElementsOnDisk="0"
            eternal="false"
            statistics="false"
            timeToIdleSeconds="60"
            timeToLiveSeconds="60"
            overflowToDisk="false"
            diskPersistent="false"
            memoryStoreEvictionPolicy="LRU"/>

    <cache
            name="NoticeReadMapper.findByPage"
            maxElementsInMemory="10000"
            maxElementsOnDisk="0"
            eternal="false"
            statistics="false"
            timeToIdleSeconds="60"
            timeToLiveSeconds="60"
            overflowToDisk="false"
            diskPersistent="false"
            memoryStoreEvictionPolicy="LRU"/>

</ehcache>

 

ehcache Configuration

@Configuration // 스프링 설정파일
@EnableCaching // 스프링 캐싱을 활성화
public class EhcacheConfiguration {

    @Bean // 캐시 매니저를 빈으로 등록
    @Primary // 동일한 타입의 빈이 여러개 있을 때, 이 빈을 우선적으로 사용하도록 설정
    public CacheManager cacheManager(EhCacheManagerFactoryBean ehCacheManagerFactoryBean) { // EhCacheManagerFactoryBean을 주입받아 캐시 매니저를 생성
        return new EhCacheCacheManager(ehCacheManagerFactoryBean.getObject()); // EhCacheCacheManager를 생성하고 EhCacheManagerFactoryBean에서 생성한 캐시 매니저를 주입
    }

    @Bean // EhCacheManagerFactoryBean을 빈으로 등록
    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() { // EhCacheManagerFactoryBean을 생성하는 빈
        EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean(); // EhCacheManagerFactoryBean 생성
        ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml")); // ehcache.xml 파일을 설정
        ehCacheManagerFactoryBean.setShared(true); // 여러 스프링 빈이 동일한 EhCacheManager 인스턴스를 공유할 수 있도록 설정
        return ehCacheManagerFactoryBean; // 생성한 EhCacheManagerFactoryBean 반환
    }

}

 

캐싱 사용

@Override // 캐시 설정
    @Cacheable(value = "NoticeReadMapper.findAll") // 캐시 이름을 지정
    public List<Notice> getAllNotices() { // 캐시할 메서드
        return noticeReadMapper.findAll(); // 캐시할 메서드의 반환값
    }

 

cache에 데이터 확인을 위한 API 생성

@RestController // REST API를 처리하는 컨트롤러
@RequestMapping("/api") // 요청 URL을 매핑
public class EhcacheController { // EhcacheController 클래스
    private CacheManager cacheManager; // 캐시 매니저

    public EhcacheController(CacheManager cacheManager)
    {
        this.cacheManager = cacheManager;
    } // 캐시 매니저를 주입받는 생성자

    @GetMapping("/ehcache") // GET 요청을 처리하는 메서드
    public Object findAll(){ // 모든 캐시를 조회하는 메서드
        List<Map<String, List<String>>> result = cacheManager.getCacheNames().stream() // 캐시 매니저에 등록된 모든 캐시 이름을 스트림으로 변환
            .map(cacheName -> { // 캐시 이름을 매핑
                EhCacheCache cache = (EhCacheCache) cacheManager.getCache(cacheName); // 캐시 매니저에서 캐시 이름에 해당하는 캐시를 가져옴
                Ehcache ehcache = cache.getNativeCache(); // 캐시에서 네이티브 캐시를 가져옴
                Map<String, List<String>> entry = new HashMap<>(); // 캐시 이름과 캐시 키를 저장할 맵 생성

                ehcache.getKeys().forEach(key -> { // 캐시의 모든 키에 대해 반복
                    Element element = ehcache.get(key); // 키에 해당하는 엘리먼트를 가져옴
                    if (element != null) { // 엘리먼트가 null이 아니면
                        entry.computeIfAbsent(cacheName, k -> new ArrayList<>()).add(element.toString()); // 캐시 이름과 캐시 키를 맵에 저장
                    }
                });

                return entry; // 맵 반환
            })
            .collect(Collectors.toList()); // 스트림을 리스트로 변환

        return result; // 결과 반환
    }
}

 

공지사항 데이터 조회

 

NoticeReadMapper.findAll의 캐시 데이터 확인

 

설정한 60초가 지나서 캐시 데이터가 사라진 결과

 

@Cacheable 옵션 활용

  • key 
    • 캐시의 키를 동적으로 생성하기 위한 SpEL(스프링 표현 언어) 식을 지정하는데 사용
    • 메소드의 파라미터를 이용하여 특정 파라미터 값을 기반으로 캐시 키 생성 가능
    • 캐시이름은 동일 할 때 각 페이지에 대한 캐시 구별을 위해서 사용
  • condition
    • 캐시가 적용되기 위한 추가적인 조건을 지정할 때 사용
    • condition에 지정된 SpEL 식이 true인 경우에만 캐시가 적용
@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); // 캐시할 메서드의 반환값
    }
  • key
    • 캐시의 키를 requestUrl 과 pageNumber를 조합해서 생성
  • condition
    • 공지사항 페이징 조회시 pageNumber가 5 이하인 경우에만 캐싱처리

 

공지사항 2페이지 데이터 조회

 

공지사항 2페이지는 condition의 조건인 5페이지 이하이므로 캐시 데이터 저장 확인

 

공지사항 6페이지 데이터

 

공지사항 6페이지는 condition 조건인 5페이지 이상 페이지이므로 캐시 데이터 저장 하지 않는다.

 

ngrinder를 통해서 캐시 적용 전과 후 부하 테스트

 

  • 전체 페이지 데이터 조회 테스
  • 캐시 적용전
    • @Cacheable(value = "NoticeReadMapper.findAll") 주석 처리하고 실행
    • Vuser(가상 사용자) : 10
    • Duration : 1분

 

  • 캐시 적용
    • @Cacheable(value = "NoticeReadMapper.findAll")를 활성화 하고 실행 
    • Vuser(가상 사용자) : 10
    • Duration : 1분

 

  • 수치 변화
    • 평균 TPS : { 23.6 } → { 331.1 }
      • 평균 TPS가 캐시 적용 전 대비 1402% 증가
    • Peek TPS : { 30 } → { 421.5 }
    • Mean Test Time : { 436.09 } ms → { 24.00 }ms
      • 평균 응답시간은 1817% 단축
    • Exected Tests : {1238} → {18031}
      • 실행된 테스트 개수는 14배 상승
  • 수치 변화를 통해 캐시 적용을 통해서 평균 TPS가 증가하고 평균 응답시간이 단축됨에 따라 성능 향상이 되었음을 알 수 있다.

 

  • 랜덤으로 페이지 번호 호출해서 페이지 하나씩 호출(1~10 랜덤 페이지 호출) 할 때의 테스트
    • 테스트 코드 수정
// 기존 코드
@Test
	public void test() {
		HTTPResponse response = request.GET("http://127.0.0.1:8080/api/notices", 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))
		}
	}


// 랜덤하게 호출 하도록 수정
@Test
	public void test() {
	// 랜덤한 페이지 값 생성 (1 이상, 10 이하)
    def randomPage = new Random().nextInt(10) + 1

    // API 호출 URL 조합
    def apiUrl = "http://127.0.0.1:8080/api/notices/${randomPage}"

    // API 호출 및 응답 획득
    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))
		}
	}



  • GET /api/notices/{pageNuumber}
  • 캐시 적용하기 전(@Cacheable(value = "NoticeReadMapper.findByPage", key = "#request.requestURI + '-' + #pageNumber", condition = "#pageNumber <= 5") // 캐시 이름과 키를 지정하고, 캐시 조건을 지정) 비활성화
    • Vuser(가상 사용자) : 10
    • Duration : 1분

 

  • 캐시 적용 후(@Cacheable(value = "NoticeReadMapper.findByPage", key = "#request.requestURI + '-' + #pageNumber", condition = "#pageNumber <= 5") // 캐시 이름과 키를 지정하고, 캐시 조건을 지정) 활성화
    • Vuser(가상 사용자) : 10
    • Duration : 1분
    • 캐싱처리는 5page 까지만 적용
    • 6page ~ 10page는 캐시 미적용

  • 수치 변화
    • 평균 TPS : { 1237.2 } → { 1608.0 } 
      • 약 1.3배 증가
    • Peek TPS : { 1822 } → { 2567.5 }
    • Mean Test Time : { 5.44 } ms → { 4.57 } ms 
      • 약 1.2배 단축
    • Exected Tests : { 67250 } → { 87362 }
      • 약 1.3배 증가

'개발 > 성능개선' 카테고리의 다른 글

비동기 방식  (0) 2024.03.10
인덱스를 타지 않는 경우  (0) 2024.03.09
Mysql Profiling으로 수치 확인  (0) 2024.03.09
인덱스 활용  (0) 2024.03.08
nGrinder 용어  (0) 2024.03.05