본문 바로가기
SpringBoot/성능 개선 흔적들

[Spring] Local Cache vs Global Cache 성능 비교 실험을 통해 배운 점

by rla124 2024. 8. 26.

여러 프로젝트에서 성능 개선을 위한 고민을 하면서 특히 조회 API의 경우 캐시를 도입해봐야겠다는 생각을 많이 했었다. 그동안의 각기 다른 방식으로 캐시를 적용해본 PR 내역을 정리하면 아래와 같다.

 

1. Local Cache Caffeine PR 링크 : 후아 프로젝트 / 키워드별 꽃 조회 API

2. Local Cache EhCache PR 링크 : 세종피어 프로젝트 / 단과대 전체 조회, 스터디 교외 카테고리 전체 조회 API

3. Global Cache Redis PR 링크 : 세종피어 프로젝트 / 단과대 전체 조회, 스터디 교외 카테고리 전체 조회 API

 

실험을 진행할 때 비교군은 동일해야하기 때문에 같은 프로젝트에서 같은 메소드에 캐시를 도입한 2번과 3번 방식을 비교해보고자 한다. 3번 방법은 이 게시글에서 성능을 언급한 바 있기 때문에 2번 사항의 성능을 소개하고 이를 비교해보고자 한다. 

 

Local Cache vs Global Cache 성능 비교 실험

3번 Global Cache Redis 를 적용했을 때 스터디 교외 카테고리 전체 조회 시 레디스 @Cacheable을 적용하여 Redis에 저장된 캐시 데이터를 얻는데 31ms의 시간이 소요되었다. 

 

2번 Local Cache EhCache을 적용했을 때 build.gradle 의존성 추가 시 스프링부트에서 지원하는 EhCache를 이용하였으며 2번의 경우 CacheManager가 RedisCacheManager였다면 2번의 경우 ehCache를 사용할 때 기본적으로 제공되는 EhCacheManager이다. 2번 Local Cache EhCache 상황에서 캐시된 데이터를 조회했을 때 성능은 16ms로 아래와 같았다. EhCache보다 Caffeine이 성능이 더 좋지만 차선책인 EhCache를 사용했음에도 Redis보다 빨랐다. 

  

 

왜 이런 결과가 나왔을까? 각 Cache 종류의 장단점을 살펴보고자 한다. 

 

Local Cache의 장단점

Local Cache는 애플리케이션 "내부" 메모리에서 직접 데이터를 조회하기 때문에 매우 응답 속도가 빠르다. 실제로 Redis보다 빨랐으며 네트워크를 경유하지 않기 때문에 네트워크 부하가 작다. 하지만 서버의 메모리 용량에 따라 캐시 가능한 데이터의 양이 제한되기 때문에 아래 ehcache.xml 코드처럼 별도의 운영체제 페이지 교체 알고리즘을 적용해야 한다. 나는 가장 적게 참조된 데이터부터 제거하는 LFU로 설정하였다. 

<?xml version="1.0" encoding="UTF-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck = "false">

    <diskStore path="java.io.tmpdir"/>

    <cache name="getAllColleges"
        maxEntriesLocalHeap="1000"
        maxEntriesLocalDisk="10000"
        eternal="false"
        diskSpoolBufferSizeMD="20"
        timeToIdleSeconds="1800"  timeToLiveSeconds="1800"
        memoryStoreEvictionPolicy="LFU"
        transactionalMode="off" >
        <persistence strategy="localTempSwap"/>
    </cache>

    <cache name="getAllExternalActivityCategories"
           maxEntriesLocalHeap="1000"
           maxEntriesLocalDisk="10000"
           eternal="false"
           diskSpoolBufferSizeMD="20"
           timeToIdleSeconds="1800"  timeToLiveSeconds="1800"
           memoryStoreEvictionPolicy="LFU"
           transactionalMode="off" >
        <persistence strategy="localTempSwap"/>
    </cache>

</ehcache>

 

특히 여기서 적용한 ehcache의 경우 분산 기능까지 제공하지만 이 프로젝트에서는 여러 대의 ec2를 운영하고 있지 않으므로 캐시 데이터의 일관성 문제를 고려할 필요가 없었다. 

 

Global Cache의 장단점 

3번에서 적용했던 Redis를 통한 캐시는 네트워크를 통해 여러 서버 인스턴스가 동일한 캐시를 공유할 수 있다는 이점이 있다. 따라서 Local Cache보다 데이터의 무결성을 더 잘 보존할 수 있다는 것이 핵심이다. 또한 메모리 기반 캐시 시스템이지만 필요에 따라 디스크에 캐시 데이터를 영속화할 수 있기 때문에 데이터 손실도 방지할 수 있다. 그러나 Global Cache는 네트워크를 통해 접근하므로 지연으로 인해 응답 시간이 늘어날 수 있으며 실제 이 게시글에서 소개하는 실험 결과에서 Local Cache가 더 속도가 빨랐던 이유가 이 때문이라고 생각한다. 내가 직접 AWS ElastiCache를 구축하였던 경험을 되살려보면 별도의 Redis-stable 설치와 네트워크 설정이 필요하기 때문에 Local Cache보다 관리/구성의 복잡성이 증가하고 클라우드 환경이기 때문에 운영 비용에 대한 고려도 해야했다.

 

내가 내린 결론

실험을 적용했던 세종피어 프로젝트처럼 단일 서버 애플리케이션으로 구성되어있으며 인스턴스의 메모리 자원이 충분히 남아있을 경우 Local Cache를 적용하는 것이 더 현명한 것 같고, 분산 시스템이 적용되어서 캐시 일관성이 중요한 요소로 작용할 경우 서버 간 캐시 공유를 위해 Global Cache를 적용하는 것이 더 적합한 것 같다는 생각이 들었다!!!

실험을 통한 오늘의 배운점~