Guava Cache

 

cache을 사용하는가?

캐시의 목적은 응답 속도의 향상을 위해서이다. 매번 DB을 쿼리하여 데이터를 가져온다면 속도가 높을 수가 없다. 이를 줄이기 위한 방법 중 하나는 DB의 성능 향상도 있겠지만 근본적이 방법은 DB 쿼리를 줄이는 것이다. , 자주 사용되는 데이터를 메모리에 저장해 두었다가 사용하면 속도가 월등히 빨라 질것이다.

캐시의 기본적인 형태는 /값 형태의 데이터 구조로 표현된다. 데이터를 호출하는 측에서 키를 캐시에 넘기면 그에 해당하는 데이터가 반환되는 구조이다.

 

 그럼 어떤 데이터를 Cache 해야 하나?

1.       캐시에 들어갈 데이터는 자주 사용하면서도 변경은 자주 일어나지 않는(즉 읽기는 많지만 쓰기는 적은) 데이터가 적합하다.

-       변경이 자주 일어나는 데이터일 경우 변경 될 때 마다 동기화 시키기 위해 캐시와 DB간의 트랜잭션이 자주 발생하여 오히려 성능 저하가 발생할 수 있다.

2.       데이터의 크기가 너무 크지 않은 것을 선택하는 것이 적합하다.

-       자주 사용하는 데이터를 Caching할수록 성능 향상에 도움이 될 것인데, 크기가 너무 크다면 그만큼 메모리를 많이 소모하게 되므로 애플리케이션의 메모리 성능에 악영향을 미칠 수 있기 때문이다.

 

Guava Cache?

 

Google Guava Library : https://github.com/google/guava

 

GoogleGuava Cache는 캐시를 쉽게 사용할 수 있도록 다양한 기능을 제공하는 오픈 소스 라이브러리이다. 간단한 코드를 통해  캐시 크기, 캐시 시간, 데이터 로딩 방법, 데이터 Refresh 방법 등을 제어할 수 있다.

Goolge Guava Apache Commons 에서 제공하지 않는 유용한 Utility성 기능들이 상당히 많다

Cache expire되더라도 DB 등의 요청은 한 번만 날라가고 그 뒤에 동시에 들어온 데이터 요청은 첫번째 요청이 끝나 캐시 데이터가 다 채워진 그 결과만 받아가게 처리하여 부하를 줄여주는 역할을 할 수 있다.

 

Cache Type

Guava에서는 2가지 타입의 cache을 제공한다.

-       LoadingCache 

n   캐시 미스가 발생하면 자동으로 데이터를 로드 한다.

n   LoadingCache.get(key)을 호출하면 key에 해당하는 데이터를 반환하는데, 데이터가 없다면 먼저 데이터 로딩을 수행한다.

-       Cache : 데이터를 자동으로 로드 하지 않는다.

위 두 타입 중에 LoadingCache을 많이 사용한다.

 

간단한 LoadingCache 구현

 

1

2

3

4

5

6

7

8

private static CacheLoader<String, String> loader = new CacheLoader<String, String>() {

@Override

    public String load(String key) throws Exception {

        return key.toUpperCase();

    }

};

 

private static LoadingCache<String, String> cache = CacheBuilder.newBuilder().build(loader);

cs

 

 

LoadingCache CacheBuilder을 이용해 구현한다. build() 메서드 호출 시 CacheLoader 구현체를 넘겨야 한다.

CacheLoader key값을 이용해 원천 데이터를 캐시에 Load하는 역할을 수행한다. CacheLoader을 구현할 때는 load() 메서드를 Override하여 원천 데이터를 가져오는 코드를 구현해야 한다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@Test

public void testGet() throws Exception {

assertThat(cache.size()).isZero();

 

// cache miss 발생한다. CacheLoader 호출한다.

String result = cache.getUnchecked("simple test");

assertThat(result).isEqualTo("SIMPLE TEST");

 

assertThat(cache.size()).isEqualTo(1);

 

// cache hit 발생한다

result = cache.getUnchecked("simple test");

assertThat(result).isEqualTo("SIMPLE TEST");

}

Colored by Color Scripter

cs

 

최초 캐시를 호출하기 전에는 캐시 사이즈가 0인 상태이다. 이후 캐시에서 값을 가져오기위해 getUnchecked("simple test");을 호출하면 캐시 miss 가 발생하여 CacheLoader을 호출하여 값을 가져오게 된다. 가져온 값은 캐시에 저장되므로 캐시의 사이즈는 1로 증가하게 된다. 이후 같은 키 값("simple test")으로 데이터를 호출하게 되면 캐시 hit이 발생하여 CacheLoader을 거치지 않고 캐시에 저장되어 있는 값을 반환하게 된다. 아래는 "simple test"키에 대한 호출이 한 번 일어난 이후의 cache 객체의 상태를 보여준다.

 

 

LocalCache key value가 저장되어 있는걸 확인할 수 있다.

 

동시성

캐시 인스턴스는 내부적으로 ConcurrentHashMap과 유사하게 구현되어 있고 thread-safe을 보장한다. 동시에 여러 개의 스레드가 같은 key에 대해서 요청을 하더라도 CacheLoader load() 메서드는 각 key에 대해 한번만 호출된다. 데이터를 요청한 모든 스레드에게 호출 결과가 반환되고, 해당 값은 캐시에 저장된다.  (using the equivalent of putIfAbsent)

 

Method

캐시에서 키와 관련된 값을 가져오는 메서드 2가지 있다.

-        get() : 데이터를 로딩하는 중 Checked Exception이 발생할 경우 ExecutionException을 던진다. 그러므로 예외 처리 코드를 반드시 작성해주어야 한다.

-       getUnchecked() : get()과 달리 CheckedException을 던지지 않는다. 그러므로 CacheLoader CheckedException을 던지지 않는 상황에서만 사용해야 한다. 예외가 발생하면 RuntimeException을 던진다.

 

Eviction

리소스 제약으로 모든 데이터를 캐시 할 수 없다. 그렇기 때문에 어떤 시점에 유지할 필요하 없는 데이터를 없애는 시점을 결정해야 한다. Guava에서는 아래와 같이 3가지 방법을 제공한다.

-       size-based eviction : 캐시 사이즈의 제한을 설정하여 제거

-       time-based eviction: 시간 기반으로 제거

-       reference-based eviction: 참조 기반으로 제거

 

1.       size-based eviction

캐시가 maximumSize (long)으로 설정한 크기 이상으로 커지면 최근에 또는 매우 자주 사용되지 않는 항목을 제거하려고 시도한다. 또는 다른 캐시 항목에 다른 "가중치"가 있는 경우 weigher(Weigher)가 있는 가중치 함수와 maximumWeight가 있는 최대 캐시 가중치를 지정할 수 있습니다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()

        .maximumSize(1000)

           .maximumWeight(100000)

           .weigher(new Weigher<Key, Graph>() {

              public int weigh(Key k, Graph g) {

                    return g.vertices().size();

              }

        })

           .build(

               new CacheLoader<Key, Graph>() {

                     public Graph load(Key key) { // no checked exception

                           return createExpensiveGraph(key);

                     }

                });

Colored by Color Scripter

cs

 

-       maximumSize(long) : 캐시에 저장될 엔트리의 개수를 설정한다. 사이즈가 100이면 100개의 키-값 쌍이 저장될 수 있다.

-       maximumWeight(long) : 캐시에 저장될 엔트리의 크기를 설정한다.

-       weigher(Weigher) : 가중치

 

2.       time-based eviction

현재 캐시에 저장되어 있는 각각의 엔트리에 대해 아래의 기준 시간 이후에 지정한 시간이 지나면 자동으로 캐시에서 삭제한다

 

1

2

3

4

5

LoadingCache<String, NaverUser> userCache = CacheBuilder

            .newBuild()

            .expireAfterAccess(10, TimeUnit.MINUTES)

            .expireAfterWrite(10, TimeUnit.MINUTES)

            .build(loader);

Colored by Color Scripter

cs

-       expireAfterAccess(long, TimeUnit)

n   엔트리가 처음으로 생성 된 시간

n   가장 최근에 엔트리의 값이 바뀐 시간

n   가장 마지막으로 접근했던 시간

-       expireAfterWrite(long, TimeUnit)

n   엔트리가 처음으로 생성 된 시간

n   가장 최근에 엔트리의 값이 바뀐 시간

 

3.       reference-based eviction

Guava를 사용하면 항목의 GC를 허용하거나 키 또는 값에 약한 참조를 사용하거나 값에 대해 부드러운 참조를 사용하여 캐시를 설정할 수 있습니다.

 

-       weakKeys(),weakValues() : 값이나 키가 weakReference로 감싸진다. reference가 없어지고 weakreference만 남아있으면 바로 GC된다..

-       softKeys(),softValues() : 값이나 키가 softReference로 감싸진다.

+ Recent posts