본문 바로가기
개발/DB

Local Cache에 대하여 (Spring Cache, Caffeine/Ehcache, Redis Client-side caching..)

by 달사쿠 2022. 9. 1.
반응형

글을 쓰게된 배경

Redis와 같은 Global Cache는 팀 내에서 거의 필수적으로 사용하고 있지만, Local cache는 자주 사용되지는 않고 있었습니다. 

사실 Local cache는 서버마다 지역적으로 데이터를 저장해 관리를 하다보니 모든 서버의 싱크를 맞추는 것은 힘들지만, 잘 변하지 않는 데이터를 가져오거나 더 높은 성능을 요구할 땐 Local cache까지도 고려해보는 것도 좋은 전략인 것 같아 알아보게 되었습니다.

 


Cache란?

먼저 Cache란, 반복적으로 데이터를 불러올 때 지속적으로 DBMS 혹은 서버에 요청하는 것이 아닌 메모리에 데이터를 저장하였다가 데이터를 불러다가 쓰는 것을 의미합니다. 그러다보니 서버나 DBMS에 부담을 덜어주고, 빠르기 때문에 많은 서비스에서 도입하여 사용하고 있습니다.

위키 백과에서는 cache를 다음과 같이 소개합니다.

캐시는 컴퓨팅에서 데이터를 저장하는 하드웨어 또는 소프트웨어 구성 요소로, 캐시에 저장된 데이터는 이전 계산의 결과이거나 다른 곳에 저장된 데이터의 복사본일 수 있습니다. 캐시 적중은 요청된 데이터를 캐시에서 찾을 수 있을 때 발생하는 반면 캐시 적중은 찾을 수 없을 때 발생합니다. 캐시 적중은 캐시에서 데이터를 읽음으로써 처리되는데, 이는 결과를 재계산하거나 느린 데이터 저장소에서 읽는 것보다 빠릅니다. 따라서 캐시에서 처리할 수 있는 요청이 많을수록 시스템은 더 빨리 수행됩니다.

비용 효율적이고 데이터를 효율적으로 사용하려면 캐시가 상대적으로 작아야 합니다. 그럼에도 불구하고 캐시는 일반적인 컴퓨터 응용 프로그램이 높은 수준의 참조 지역성으로 데이터에 액세스하기 때문에 컴퓨팅의 많은 영역에서 그 자체로 입증되었습니다. 이러한 액세스 패턴은 최근 이미 요청된 데이터가 요청되는 시간적 지역성과 이미 요청된 데이터에 물리적으로 가깝게 저장된 데이터가 요청되는 공간적 지역성을 나타낸다.

Long Tail 법칙

캐시를 어떨 때 사용하면 좋을지에 대한 고찰은 Long Tail 법칙을 적용해서 생각해보면 좋을 것 같습니다.

 

컴퓨터 공학에서는 20%의 요구사항이 80%의 리소스를 잡아먹는다는 의미로 사용됩니다.

실제 서비스에서 자주 호출되는 20%의 리소스를 캐싱함으로써 리소스 사용량을 대폭 줄이고, 성능을 대폭 향상할 수 있게 됩니다.

(더 자세한 내용은 아래를 참고하시면 좋을 것 같습니다)

http://egloos.zum.com/js7309/v/11143838

 

[JAVA]EhCache

Ehcache레퍼런스의 근원(Locality of Reference)기존 전통적인 경제법칙으로 유명한 파레토의 법칙은 상위 20퍼센트가 매출액의 80퍼센트를 점한다는 법칙이다.예를 들어 2000만원 하는 자동차를 80명이

egloos.zum.com

 


Local Cache vs Global Cache

본격적으로 Local Cache와 Spring Cache에 대해서 알아보기 전에, Local Cache와 Global Cache의 차이점에 대해서 알아보겠습니다.

주로 Local Cache로는 본 포스팅에서 소개할 Ehcache나 Caffeine이 여기에 속하며, Global Cache로는 Memcached, Redis가 여기에 속합니다.

 

반응형

Local Cache

  • Local 장비 내에서만 사용 되는 캐시
  • Local 장비의 Resource를 사용합니다. (Memory, Disk)
  • Local 내에서만 사용하기 때문에 속도가 빠릅니다.
  • 다른 서버와 데이터 공유가 어렵습니다.

Global Cache

  • 여러 서버에서 별도의 Cache Server에 접근해서 사용합니다.
  • 네트워크 트래픽을 사용하기 때문에 Local Cache 보다는 느립니다.
  • 데이터를 분산하여 저장할 수 있습니다.
    • Replication: 두 개의 이상의 DBMS 시스템을 Master / Slave로 나눠서 동일한 데이터를 저장하는 방법
    • Sharding: 여러 데이터베이스에 동일한 테이블의 스키마를 분산하여 저장하는 방법
  • 별도의 Cache Server를 이용하기 때문에, 어떤 서버에서든지 같은 데이터를 바라볼 수 있습니다.

 


Spring Cache Abstraction

 

Integration

As a lightweight container, Spring is often considered an EJB replacement. We do believe that for many, if not most, applications and use cases, Spring, as a container, combined with its rich supporting functionality in the area of transactions, ORM and JD

docs.spring.io

이제 Spring 환경에서 캐시를 적용할 수 있는 방법에 대해서 소개해보겠습니다.

스프링에서는 빈의 메소드에 캐시를 적용할 수 있는 기능을 제공합니다.

 

스프링의 캐시 추상화는 캐시 특정 기술에 종속되지 않으며 AOP를 통해 적용되어

애플리케이션 코드를 수정하지 않고 캐시 부가기능을 추가할 수 있습니다.

 

즉, 캐시 API를 코드에 추가하지 않아도 손쉽게 캐시 기능을 부여할 수 있습니다.

또한 캐시 서비스 구현 기술에 종속되지 않도록 추상화 서비스를 제공하기 때문에 환경이 바뀌거나 적용할 기술을 변경해서 캐시 서비스의 종류가 달라지더라도 애플리케이션 코드에 영향을 주지 않습니다.

쉽게말해 캐싱이 필요한 비즈니스 로직에서 EhCache, Caffeine 등 캐싱 종류에 의존하지 않고 추상화된 인터페이스로 캐싱을 적용 할 수 있습니다.

 

이 캐시 추상화는 캐시 기술을 지원하는 캐시 매니저를 Bean으로 등록해야 합니다.

 

Cache Manager

✔️ ConcurrentMapCacheManager

자바 내 Multi-Thread 환경에서 사용할 수 있는 Map인 ConcurrentHashMap을 이용해 캐시 기능을 구현하는 간단한 캐시 매니저입니다. 이름부터 Concurrent해 안전한 느낌..
Spring-boot-starter-cache를 사용했을 때, 아무 설정을 하지 않으면(Default) ConcurrentHashMap을 통해 캐싱이 됩니다.

 

하지만, Map의 구현체이다보니 캐시 관리에서 필요한 다양한 기능들이 부족합니다.
대표적으로 TTL, TTI 등을 이용해서 쓰이지 않는 데이터들에 대한 삭제 정책이 빈약하며, 이런 기능을 사용하기 위해 더 발전된 EhCache나 Caffeine 등을 사용합니다.
(ConcurrentHashMap의 캐시 정리 자체가 불가능한 것은 아닙니다. 직접 호출하거나 구현해야 할 뿐 모두 가능합니다! )

 

✔️ SimpleCacheManager

기본적으로 제공하는 캐시가 없습니다.

사용할 캐시를 직접 등록하여 사용하기 위한 캐시 매니저입니다.

 

✔️ EhCacheCacheManager

 

Ehcache

Ehcache is an open source, standards-based cache that boosts performance, offloads your database, and simplifies scalability. It's the most widely-used Java-based cache because it's robust, proven, full-featured, and integrates with other popular libraries

www.ehcache.org

자바에서 유명한 캐시 프레임워크 중 하나인 EhCache를 지원하는 캐시 매니저입니다.

ConcurrentHashMap과 차이점은 off-heap 을 설정할 수 있다는 것입니다.
위의 ConcurrentHashMap를 사용하면 on-heap 즉, Java 힙에 올라갑니다. 그렇게되면 ConcurrentHashMap은 힙에 올라가긴 하지만, 스스로 정리되지 않는다는 단점이 있습니다. 즉, 사용자가 직접 사용되지 않는 부분을 직접 삭제시켜주지 않는다면 메모리가 낭비될 수 있습니다.

Ehcache는 이러한 부분을 위해 off-heap을 지원해 인메모리 처럼 RAM에 데이터를 저장 할 수 있도록 지원하고, TTL, expiry를 통해 만료기한을 설정할 수 있습니다. 하지만, RAM은 비싸고 보통 적기 때문에.. 할당을 신중하게 해주어야 합니다.

 

부가적으로 Ehcache는 서버 간 분산캐시(Cache Clustering) 도 지원하기 때문에, 어플리케이션이 여러개의 노드에 올라가있는 상황에서 한 노드의 캐시에 변화가 생기면 나머지 노드에 그 변경 내용을 전달하여 같은 상태로 유지할 수도 있습니다.

각 노드간 캐시 데이터 전송은 자바에서 기본적으로 제공하는 원격 메커니즘인 RMI(Remote Method Invocation)를 통해 이루어지며, 변경 내용을 전달할 때는 동기/비동기 방식으로 전달이 가능합니다.

이 기능은 서버간에 데이터를 맞출필요가 있을 때 사용하면 좋을 것 같습니다.

 

* RMI에 대한 글은 https://0yumin.tistory.com/16 을 참고하시면 좋을 것 같습니다

* Ehcache의 분산 캐시는 https://oingdaddy.tistory.com/386 글을 참고하시면 좋을 것 같습니다.

 

 

✔️ CaffeineCacheManager

Java 8로 Guava 캐시를 재작성한 Caffeine 캐시를 사용하는 캐시 매니저입니다.

EhCache와 함께 인기 있는 캐시 매니저인데, EhCache보다 좋은 성능을 갖는다고 해서 최근 많이 사용합니다.

자세한 건 아래에서 설명하겠습니다.

 

✔️ CompositeCacheManager

한 개 이상의 캐시 매니저를 사용하도록 지원해주는 혼합 캐시 매니저입니다.

 

✔️ JCacheCacheManager

JSR-107 기반의 캐시를 사용하는 캐시 매니저입니다.

 

Spring Cache를 사용하는 방법은 https://gngsn.tistory.com/157 을 참고해주세요.

 


Caffeine

https://gngsn.tistory.com/158

 

GitHub - ben-manes/caffeine: A high performance caching library for Java

A high performance caching library for Java. Contribute to ben-manes/caffeine development by creating an account on GitHub.

github.com

공식 wiki에 따르면, Caffeine은 거의 최적의 적중률을 제공하는 고 성능 Java Cache 라이브러리라고 합니다.

Benchmarks 툴로 ConcurrentLinkedHashMap과 Guava, Ehcache, Infinispan과 같은 많이 사용하는 비슷한 로컬캐시들과 비교한 자료는 아래와 같은데요. 캐시의 용량 제한이 없고, 완전히 채워지며 항상 일정한 값을 계산하는 전제로 진행했을 때 Caffeine이 월등한 성능을 갖고 있다는 것을 확인할 수 있습니다.

아래는  초당 작업량 대비 데이터 처리량에 대한 자료입니다.

https://github.com/ben-manes/caffeine/wiki/Benchmarks

 

GitHub - ben-manes/caffeine: A high performance caching library for Java

A high performance caching library for Java. Contribute to ben-manes/caffeine development by creating an account on GitHub.

github.com

 

기본적으로 Caffeine은 `Window TinyLFU` (*LFU-Least Frequently Used참조 횟수가 가장 작은 페이지 교체)라는 페이지 교체 알고리즘을 Eviction 정책으로 채택하고 있습니다.

`Window TinyLfu` 정책은 거의 최적의 적중률을 제공하고, 메모리 설치 공간도 낮게 갖고 있다고 합니다.

Window TinyLfu와 관련된 내용은 https://github.com/ben-manes/caffeine/wiki/Efficiency 을 참고하면 좋을 것 같습니다.

 

이 Window TinyLfu 정책을 사용해서, 아래 세가지 타입으로 캐시를 Evict하는 설정을 할 수 있습니다.

 

Size-Based

Caffeine.newBuilder().maximumSize(long)

크기 기준으로 캐시를 제거하는 방식입니다.

개발자가 설정한 특정 값을 기준으로, entries의 크기가 그 값을 넘을 때 entries의 일부분을 제거합니다. 이때 제거되는 값은 가장 최근에 사용되지 않았거나, 자주 사용되어지지 않는 값입니다.

 

Time-based

Caffeine.newBuilder().expireAfterAccess(long)
Caffeine.newBuilder().expireAfterWrite(long[, TimeUnit])
Caffeine.newBuilder().expireAfter(Expiry)

✔ expireAfterAccess

(캐시 생성 이후) 해당 값이 가장 최근에 대체되거나 마지막으로 읽은 후 특정 기간이 지나면 각 항목이 캐시에서 자동으로 제거되도록 지정합니다.

 

✔ expireAfterWrite

캐시 생성 후 또는 가장 최근에 바뀐 후 특정 기간이 지나면 각 항목이 캐시에서 자동으로 제거되도록 지정합니다.

 

✔ expireAfter

캐시가 생성되거나 마지막으로 업데이트된 후 지정된 시간 간격으로 캐시를 새로 고침합니다.

 

Reference-based

Caffeine.newBuilder().weakKeys().weakValues()
Caffeine.newBuilder().softValues()

✔ Caffeine.weakKeys(), Caffeine.weakValues()

Week References를 사용하여 키를 저장합니다.

Week References는 키에 대한 다른 강력한 참조(Strong References)가 없는 경우 가비지 수집할 수 있습니다.

가비지 수집은 identity에만 의존하므로 전체 캐시가 동등성(.equals()) 대신 identity 동일성(==)을 사용하여 키를 비교합니다.

 

✔ Caffeine.softValues()

Soft References를 사용하여 값을 저장합니다.

Soft References 오브젝트는 메모리 수요에 따라 least-recently-used 방식으로 가비지가 수집됩니다.

소프트 레퍼런스를 사용하면 퍼포먼스가 영향을 받기 때문에 일반적으로 예측 가능한 최대 캐시 크기를 사용하는 것이 좋습니다. softValues()를 사용하면 Week References와 마찬가지로 값이 equals 대신 identity(==) equality를 사용하여 비교됩니다.

 

 

Caffeine의 기능과 관련된 더 자세한 내용은 공식위키나 https://gngsn.tistory.com/158 를 참고해도 좋을 것 같습니다.

 


Redis Client-Side Caching

마지막으로 redis 6에서 새로 등장한 Redis Client-Side Caching에 대해서 소개하고 마무리해보겠습니다.

레디스 서버는 고성능(high performance)이지만, 클라이언트(애플리케이션) 입장에서 보면 데이터가 필요할 때마다 매번 레디스 서버에 쿼리해서 가져오는 것보다 한번 가져온 데이터를 계속 사용할 수 있다면 훨씬 더 나은 성능을 제공할 수 있을 것입니다.
이 경우 해결해야 할 문제는 데이터가 변경되었을 때 클라이언트는 해당 데이터가 변경되었는지를 어떻게 아느냐하는 것입니다. 레디스 서버 입장에서는 변경된 데이터를 조회해서 가지고 있는 클라이언트들에게 데이터가 변경되었음을 알려주는 효율적인 방법이 있어야 합니다.
이 Reids Client-Side Caching은 레디스가 pub/sub 기능을 이용해서 캐싱을 사용하는 클라이언트에 알림을 보냅니다.
이 Reids Client-Side Caching은 다른 말로 tracking이라고 불리며, 기본 모드와 브로드캐스팅(broadcasting) 모드가 있습니다.

  • 기본 모드에서 서버는 클라이언트가 액세스한 키를 기억하고, 동일한 키가 수정될 때 클라이언트로 데이터를 삭제하라는 메세지를 보냅니다. 이는 서버측에서 메모리 비용이 들지만(단점), 정확히 클라이언트가 가지고 있는 키에 대해서만 무효 메시지를 보낼 수 있는 장점이 있습니다.
  • 브로드캐스팅 모드에서 레디스 서버는 키에 대한 값은 저장하지 않습니다. 대신 키의 앞부분 프리픽스와, 매칭되는 키를 가지고 있는 클라이언트를 저장합니다. 프리픽스에 해당하는 클라이언트는 해당하는 키가 변경될 때마다 서버로부터 알림을 받고, 매칭되는 키가 있다면 캐싱되어있는 그 데이터를 삭제합니다. 이 방법은 기본 모드보다 메모리는 더 적게 사용하지만, 해당 키가 없는 클라이언트도 이 값을 수신할 수 있기 떄문에 네트워크 사용량이 더 증가할 수 있습니다.

출처: https://meetup.toast.com/posts/245

더 자세한 내용은 아래 링크들을 참고하면 좋을 것 같습니다.

 

NHN에서 작성한 자료: https://meetup.toast.com/posts/245

Redis 공식 docs: https://redis.io/docs/manual/client-side-caching/

Redis 공식 docs 번역: https://medium.com/garimoo/%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%82%AC%EC%9D%B4%EB%93%9C-%EC%BA%90%EC%8B%B1-71a3ca7727ff. 

 

더보기

부가적으로 Cache 사용으로 인해 발생하는 이슈들은 아래와 같습니다.

  • Look-Aside caching(값이 없을때 원본 데이터를 DB와 같은 곳에서 읽어와서 채우는것)으로 인한 race condition 발생 가능.
  • thundering herd 이슈 -> expire time보다 조금 더 빨리 재계산해줘서 해당 값을 남아있게 한다. 또는 캐시 락킹 방법사용 가능
  • 캐시서버들 간에 데이터가 고루 분배되지 않아 특정 서버에만 캐시가 많이 쌓이는 이슈 -> adpative consistency개념 적용 가능
  • 메모리는 비싸다는 단점 -> SDS와 같은 좋은 장비에 cold 데이터를 보관하고 hot한 데이터만 메모리에 두는 방법으로도 해결가능
반응형

댓글