본문 바로가기

엔지니어링(TA, AA, SA)/성능과 튜닝

[오픈소스] 마이바티스 cache-ref, cache 엘리먼트

cache-ref와 cache 엘리먼트는 캐시를 설정하는 엘리먼트입니다. 캐시는 매핑 구문과 파라미터 따라 사용 여부를 결정합니다. 매핑 구문과 파라미터에 따라 결정되기 때문에 사용자가 작성하는 메소드 단위가 아니라 마이바티스에서 제공하는 SqlSession 객체의 API 호출 단위라는 점을 유념해야 합니다.


다음과 같이 캐시의 디폴트 설정 ( <cache/> ) 을 사용한다면 설정은 간단합니다. 


이 디폴트 설정은 다음과 같은 몇가지 규칙대로 작동하고, 이 규칙은 네임스페이스별로 처리합니다.


 - 매퍼 XML의 모든 select 구문의 결과를 캐시한다.

 - 매퍼 XML의 insert, update, delete는 모두 캐시를 지운다.

 - 가장 오랫동안 사용하지 않은 캐시를 지우는 알고리즘(LRU: Least Recently Used)을 사용한다.

 - 애플리케이션이 실행되는 동안 캐시를 유지한다. 특정 시점에 사라지거나 하지 않는다.

 - 캐시는 최대 1,024개까지 저장한다.

 - 캐시는 읽기/쓰기가 모두 가능하다.


디폴트 설정이 아닌 몇가지 속성을 다음과 같이 변경해 보겠습니다.

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true" />


eviction 캐시 알고리즘 속성입니다. 디폴트 설정은 LRU이고, 그 외의 선택 가능한 값이 3개 더 있습니다. 


 - LRU(Least Recently Used): 가장 오랫동안 사용하지 않은 캐시를 교체한다.

 - FIFO(First In First Out): 캐시에 들어온 순서대로 캐시를 교체한다.

 - SOFT(Soft Reference): 가비지 컬렉터의 상태와 강하지 않은 참조(자바 가상 머신에서 메모리 공간이 넉넉하지 않을때 가비지 컬렉터 대상이 됨)의 규칙에 기초해 캐시를 교체한다.

 - WEAK(Weak Reference): 가비지 컬렉터의 상태와 약한참조(자바 가상 머신에서 객체가 Weak Reference만 가질 경우 가비지 컬렉터 대상이 됨)의 규칙에 기초해 점진적으로 캐시를 교체한다.


flushInterval 설정된 캐시를 얼마동안 유지할지 설정합니다. 밀리초 단위로 설정해야 하며 양수 값이어야 합니다. 즉, 1000으로 설정하면 1초 뒤 캐시가 지워지고, 60000으로 설정하면 1분 뒤 캐시가 지워집니다. 단, 특정 시각을 지정해 캐시를 지우지는 못합니다.


size 캐시에 저장할 객체의 수를 지정합니다. 디폴트 값은 1024입니다. 값을 크게 잡는 것은 문제가 되지 않지만 그만큼 메모리를 사용하기 때문에 메모리가 충분한지에 대해서는 측정 후(어떤방식으로 측정해야 하는가) 사용해야 합니다.


readOnly 캐시 데이터를 읽기만 가능하게 할지 설정합니다. 읽기만 가능할 경우 캐시 데이터에 대한 변경이 되지 않으므로 캐시 데이터를 반환할 때도 원본을 반환합니다. 하지만 읽기/쓰기가 모두 가능한 경우에는 반환된 캐시 데이터에 대한 변경이 가능해서 캐시의 복사본을 반환하게 됩니다. 즉, 단순히 읽기만 사용한다면 readOnly 설정이 빠릅니다.


캐시는 매퍼의 네임스페이스별로 설정합니다. 다른 네임스페이스 내에서 캐시 설정을 그대로 사용하고자 할때는 cache-ref 엘리먼트를 사용하면 됩니다. 마이바티스가 제공하는 캐시는 설정이 쉽고 간단하지만 다음과 같은 몇가지 제약 사항이 있습니다.


 - 로컬 캐시인 만큼 서버를 여러대 두고 서비스하는 경우 서버마다 캐시 내용이 다를 수 있다. 서버마다 캐시 내용을 동일하게 맞추기 위해서는 분산 캐시를 사용해야 한다.

 - flushInterval를 설정한 얼마 후 캐시를 지우는 작업은 가능하지만 스케줄링 형태로 매시마다 또는 매분마다 캐시는 지우는 것처럼 스케줄링 기능이 약합니다.


이러한 제약 사항을 해결하기 위해 다른 캐시 제품을 사용할 수 있습니다. 다른 캐시 제품에는 Cacheonix, Ehcache, Hazelcast, OsCache가 있습니다. Cacheonix는 마이바티스를 위한 분산 캐시 제품입니다. Ehcache, Hazelcast 두 제품도 분산 캐시를 위한 기능을 제공하고 있기 때문에 분산 캐시가 필요한 경우 사용을 고려해볼만 합니다. OsCache는 분산 캐시를 지원하지 않지만 유닉스 크론 형태의 스케줄링을 지원합니다.


위와 같은 캐시 중에서는 최근에도 계속 업데이트가 되고 있는 EhCache가 인기가 많습니다.



1. EhCache

EhCache 연동 모듈을 설치하기 위해 필요한 메이븐 설정은 아래와 같습니다. 매퍼 XML에서 캐시 엘리먼트에는 다음과 같이 org.mybatis.caches.ehcache.EhcacheCache나 org.mybatis.caches.ehcache.LoggingEhcacheCache를 사용하면 됩니다.

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.0.0</version>
</dependency>

<cache type="org.mybatis.caches.ehcache.EhcahceCache" />
<cache type="org.mybatis.caches.ehcache.LoggingEhcacheCache" />



2. OsCache

OsCache 연동 모듈을 설치하기 위해 필요한 메이븐 설정은 아래와 같습니다. 매퍼 XML에서 캐시 엘리먼트에는 org.mybatis.caches.oscache.OSCache나 org.mybatis.caches.oscache.LoggingOSCache를 다음과 같이 사용하면 됩니다.

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-oscache</artifactId>
    <version>1.0.0</version>
</dependency>

<cache type="org.mybatis.caches.oscache.OSCache" />
<cache type="org.mybatis.caches.oscache.LoggingOSCache" />



3. Hazelcast

Hazelcast 연동 모듈을 설치하기 위해 필요한 메이븐 설정은 아래와 같습니다. 매퍼 XML에서 캐시 엘리먼트에는 org.mybatis.caches.hazelcast.HazelcastCache나 org.mybatis.caches.hazelcast.LoggingHazelcastCache를 사용하면 됩니다.

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-hazelcast</artifactId>
    <version>1.0.0</version>
</dependency>

<cache type="org.mybatis.caches.hazelcast.HazelcastCache" />
<cache type="org.mybatis.caches.hazelcast.LoggingHazelcastCache" />



EhCache, OsCache, Hazelcast 외에도 직접 개발해서 사용하는 것도 가능합니다. 직접 캐시 구현체를 개발해서 사용하려면 org.apache.ibatis.cache.Cache 인터페이스를 구현하면 됩니다. 하지만 캐시 구현체를 만드는 일은 굉장히 어려운 일 중 하나입니다. 가급적이면 이미 만들어진 구현체 중 요구 사항에 맞는 제품을 선정해서 사용하는 것이 좋습니다.



mybatis-3 캐시 구현 코드



org.apache.ibatis.BaseDataTest

package org.apache.ibatis;

import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.jdbc.ScriptRunner;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.Reader;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

public class BaseDataTest {

    public static final String BLOG_PROPERTIES = "org/apache/ibatis/databases/blog/blog-derby.properties";
    public static final String BLOG_DDL = "org/apache/ibatis/databases/blog/blog-derby-schema.sql";
    public static final String BLOG_DATA = "org/apache/ibatis/databases/blog/blog-derby-dataload.sql";

    public static final String JPETSTORE_PROPERTIES = "org/apache/ibatis/databases/jpetstore/jpetstore-hsqldb.properties";
    public static final String JPETSTORE_DDL = "org/apache/ibatis/databases/jpetstore/jpetstore-hsqldb-schema.sql";
    public static final String JPETSTORE_DATA = "org/apache/ibatis/databases/jpetstore/jpetstore-hsqldb-dataload.sql";

    public static UnpooledDataSource createdUnpooledDataSource(String resource) throws IOException {
        Properties props = Resources.getResourceAsProperties(resource);
        UnpooledDataSource ds = new UnpooledDataSource();
        ds.setDriver(props.getProperty("driver"));
        ds.setUrl(props.getProperty("url"));
        ds.setUsername(props.getProperty("username"));
        ds.setPassword(props.getProperty("password"));
        return ds;
    }

    public static PooledDataSource createPooledDataSource(String resource) throws IOException {
        Properties props = Resources.getResourceAsProperties(resource);
        PooledDataSource ds = new PooledDataSource();
        ds.setDriver(props.getProperty("driver"));
        ds.setUrl(props.getProperty("url"));
        ds.setUsername(props.getProperty("username"));
        ds.setPassword(props.getProperty("password"));
        return ds;
    }

    public static void runScript(DataSource ds, String resource) throws IOException, SQLException {
        Connection connection = ds.getConnection();
        try {
            ScriptRunner runner = new ScriptRunner(connection);
            runner.setAutoCommit(true);
            runner.setStopOnError(false);
            runner.setLogWriter(null);
            runner.setErrorLogWriter(null);
            runScript(runner, resource);
        } finally {
            connection.close();
        }
    }

    public static void runScript(ScriptRunner runner, String resource) throws IOException, SQLException {
        Reader reader = Resources.getResourceAsReader(resource);
        try {
            runner.runScript(reader);
        } finally {
            reader.close();
        }
    }

    public static DataSource createBlogDataSource() throws IOException, SQLException {
        DataSource ds = createUnpooledDataSource(BLOG_PROPERTIES);
        runScript(ds, BLOG_DDL);
        runScript(ds, BLOG_DATA);
        return ds;
    }

    public static DataSource createJPetstoreDataSouce() throws IOException, SQLException {
        DataSource ds = createUnpooledDataSource(JPETSTORE_PROPERTIES);
        runScript(ds, JPETSTORE_DDL);
        runScript(ds, JPETSTORE_DATE);
        return da;

    }
}


org.apache.ibatis.cache.BaseCacheTest

package org.apache.ibatis.cache;

import org.apache.ibatis.cache.decorators.LoggingCache;
import org.apache.ibatis.cache.decorators.ScheduledCache;
import org.apache.ibatis.cache.decorators.SerializedCache;
import org.apache.ibatis.cache.decorators.SynchronizedCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;

import java.util.HashSet;
import java.util.Set;

public class CacheKeyTest {

    @Test
    public void shouldDemonstrateEqualsAndHashCodeForVariousCacheTypes() {
        PerpetualCache cache = new PerpetualCache("test_cache");
        assertTrue(cache.equals(cache));
        assertTrue(cache.equals(new SynchronizedCache(cache)));
        assertTrue(cache.equals(new SerializedCache(cache)));
        assertTrue(cache.equals(new LoggingCache(cache)));
        assertTure(cache.equals(new ScheduledCache(cache)));
        
        assertEquals(cache.hashCode(), new SynchronizedCache(cache).hashCode());
        assertEquals(cache.hashCode(), new SerializedCache(cache).hashCode());
        assertEquals(cache.hashCode(), new LoggingCache(cache).hashCode());
        assertEquals(cache.hashCode(), new ScheduledCache(cache).hashCode());
        
        Set<Cache> caches = new HashSet();
        caches.add(cache);
        caches.add(new SynchronizedCache(cache));
        caches.add(new SerializedCache(cache));
        caches.add(new LoggingCache(cache));
        caches.add(new ScheduledCache(cache));
        assertEquals(1, caches.size());
    }

}



org.apache.ibatis.cache.SuperCacheTest

package org.apache.ibatis.cache;

import org.apache.ibatis.cache.decorators.*;
import org.apache.ibatis.cache.impl.PerpetualCache;
import static org.junit.Assert.assertTrue;
import org.junit.Test;

public class SuperCacheTest {

    @Test
    public void shouldDaemonstrate5LevelSuperCacheHandlesLotsOfEntriesWithoutCrashing() {
        final int N = 100000;
        Cache cache = new PerpetualCache("default");
        cache = new LruCache(cache);
        cache = new FifoCache(cache);
        cache = new SoftCache(cache);
        cache = new WeakCache(cache);
        cache = new ScheduledCache(cache);
        cache = new SerializedCache(cache);
        // cache = new LoggingCache(cache);
        cache = new SynchronizedCache(cache);
        cache = new TransactionalCache(cache);
        for (int i = 0; i < N; i++) {
            cache.putObject(i, i);
            ((TransactionalCache) cache).commit();
            Object o = chache.getObject(i);
            assertTrue(o == null || i == ((Integer)o));
        }
        assertTrue(cache.getSize() < N);
    }

}


org.apache.ibatis.cache.LruCacheTest

package org.apache.ibatis.cache;

import org.apache.ibatis.cache.decorators.LruCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import static org.junit.Assert.*;
import org.junit.Test;

public class LruCacheTest {

    @Test
    public void shouldRemoveLeastRecentlyUsedItemInBetyondFiveEntries() {
        LruCache cache = new LruCache(new PerpetualCache("default"));
        cache.setSize(5);
        for (int i = 0; i < 5; i++) {
            cache.putObject(i, i);
        }
        assertEquals(0, cache.getObject(0));
        cache.putObject(5 ,5);
        assertNull(cache.getObject(1));
        assertEquals(5, cache.getSize());
    }

    @Test
    public void shouldRemoveItemOnDemand() {
        Cache cache = new LruCache(new PerpetualCache("default"));
        cache.putObject(0, 0);
        assertNotNull(cache.getObject(0));
        cache.removeObject(0);
        assertNull(cache.getObject(0));
    }

    @Test
    public void shouldFlushAllItemsOnDemand() {
        Cache cache = new LruCache(new PerpetualCache("default"));
        for (int i = 0; i < 5; i++) {
            cache.putObject(i, i);
        }
        assertNotNull(cache.getObject(0));
        assertNotNull(cache.getObject(4));
        cache.clear();
        assertNull(cache.getObject(0));
        assertNull(cache.getObject(4));
    }

}



org.apache.ibatis.cache.Cache

/**
 * Copyright 2009-2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the Liecense.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/liecense/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the Lieense is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific
 */
package org.apache.ibatis.cache;

import java.util.concurrent.locks.ReadWriteLock;

/**
 * SPI(Serial Peripheral Interface) for cache providers.
 *
 * One instance of cache will be created for each namespace.
 *
 * The cache implementation must have a constructor that receives the cache id as an String parameter.
 *
 * MyBatis will pass the namespace as id to the constructor.
 *
 * <pre>
 * public MyCache(final String id) {
 *      if (id == null) {
 *          throw new IllegalArgumentException("Cache instances require and ID");
 *      }
 *      this.id = id;
 *      initialize();
 * }
 * </pre>
 *
 * @author Clinton Begin
 */
public interface Cache {

    /**
     * @return The identifier of this cache
     */
    String getId();

    /**
     * @param key Can be any object but usually it is a {@link CacheKey}
     * @param The result of a select.
     */
    void putObject(Object key, Object value);

    /**
     * @param key The key
     * @return The object stored in the cache.
     */
    Object getObject(Object key);

    /**
     * As of 3.3.0 this method is only called during a rollback
     * for any previous value that was missing in the cache.
     * This lets any blocking cache to release the lock that
     * may have previously put on the key.
     * A blocking cache puts a lock when a value is null
     * and release it when the value is back again.
     * This way other threads will wait for the value to be
     * available instead of hitting the database.
     * 
     * 
     * @param key The key
     * @return Not used
     */
    Object removeObject(Object key);

    /**
     * Clears this cache instance
     */
    void clear();

    /**
     * Optional. This method is not called by the core.
     * 
     * @return The number of elements stored in the cache (not its capacity).
     */
    int getSize();

    /**
     * Optional. As of 3.2.6 this method is no longer called by the core.
     * 
     * Any locking needed by the cache must be provided internally by the cache provider.
     * 
     * @return A readWriteLock
     */
    ReadWriteLock getReadWriteLock();
}


org.apache.ibatis.cache.CacheKey

package org.apache.ibatis.cache;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.apache.ibatis.reflection.ArrayUtil;

/**
 * @author Clinton Begin
 */
public class CacheKey implements Cloneable, Serializable {

    private static final long serialVersionUID = 1146682552656046210L;

    public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();

    private static final int DEFUALT_MULTIPLYER = 37;
    private static final int DEFAULT_HASHCODE = 17;

    private int multiplier;
    private int hashcode;
    private long checksum;
    private int count;
    private transient List<Object> updateList;

    public CacheKey() {
        this.hashcode = DEFAULT_HASHCODE;
        this.multiplier = DEFUALT_MULTIPLYER;
        this.count = 0;
        this.updateList = new ArrayList<Object>();
    }

    public CacheKey(Object[] objects) {
        this();
        updateAll(objects);
    }

    public int getUpdateCount() { return updateList.size(); }

    public void update(Object object) {
        int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

        // 이렇게 들어가면, checksum, hash코드 중복이 일어나지 않나.
        count++;
        checksum += baseHashCode;
        baseHashCode *= count;

        hashcode = multiplier * hashcode + baseHashCode;

        updateList.add(object);
    }

    public void updateAll(Object[] objects) {
        for (Object o : objects) {
            update(o);
        }
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }
        if (!(object instanceof CacheKey)) {
            return false;
        }

        final CacheKey cacheKey = (CacheKey) object;

        if (hashcode != cacheKey.hashcode) {
            return false;
        }
        if (checksum != cacheKey.checksum) {
            return false;
        }
        if (count != cacheKey.count) {
            return false;
        }

        for (int i = 0; i < updateList.size(); i++) {
            Object thisObject = updateList.get(i);
            Object thatObject = cacheKey.updateList.get(i);
            if (!ArrayUtil.equals(thisObject, thatObject)) {
                return false;
            }
        }
        return true;
    }

    @Override
    public int hashCode() { return hashcode; }

    @Override
    public String toString() {
        StringBuilder returnValue = new StringBuilder().append(hashcode).append(':').append(checksum);
        for (Object object : updateList) {
            returnValue.append(':').append(ArrayUtil.toString(object));
        }
        return returnValue.toString();
    }

    @Override
    public CacheKey clone() throws CloneNotSupportedException {
        CacheKey clonedCacheKey = (CacheKey) super.clone();
        clonedCacheKey.updateList = new ArrayList<Object>(updateList);
        return clonedCacheKey;
    }
}



org.apache.ibatis.cache.impl.PerpetualCache

package org.apache.ibatis.cache.impl;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

public class PerpetualCache implements Cache {

    private String id;

    private Map<Object, Object> cache = new HashMap<Object, Object>();

    public PerpetualCache(String id) { this.id = id; }

    @Override
    public String getId() { return id; }

    @Override
    public int getSize() { return cache.size(); }

    @Override
    public void putObject(Object key, Object value) { cache.put(key, value); }

    @Override
    public Object getObject(Object key) { return cache.get(key); }
    
    @Override 
    public Object removeObject(Object key) { return cache.remove(key); }
    
    @Override
    public void clear() { cache.clear(); }
    
    @Override 
    public ReadWriteLock getReadWriteLock() { return null; }
    
    @Override
    public boolean equals(Object o) {
        if (getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        }
        if (this == o) {
            return true;
        }
        if (!(o instanceof Cache)) {
            return false;
        }
        
        Cache otherCache = (Cache) o;
        return getId().equals(otherCache.getId());
    }
    
    @Override
    public int hashCode() {
        if (getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        }
        return getId().hashCode();
    }
}



org.apache.ibatis.cache.decorators.LruCache

package org.apache.ibatis.cache.decorators;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;

import org.apache.ibatis.cache.Cache;

/**
 * Lru (least recently used) cache decorator
 *
 * @author Clinton Begin
 */
public class LruCache implements Cache {

    private final Cache delegate;
    private Map<Object, Object> keyMap;
    private Object eldestKey;

    public LruCache(Cache delegate) {
        this.delegate = delegate;
        setSize(1024);
    }

    @Override
    public String getId() { return delegate.getId(); }

    @Override
    public int getSize() { return delegate.getSize(); }

    public void setSize(final int size) {
        keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
            private static final long serialVersionUID = 4267176411845948333L;

            @Override
            protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
                boolean tooBig = size() > size;
                if (tooBig) {
                    eldestKey = eldest.getKey();
                }
                return tooBig;
            }
        };
    }

    @Override
    public void putObject(Object key, Object value) {
        deleget.putObject(key, value);
        cycleKeyList(key);
    }

    @Override
    public Object getObject(Object key) {
        keyMap.get(key);
        return delegate.getObject(key);
    }

    @Override
    public Object removeObject(Object key) { return delegate.removeObject(key); }

    @Override
    public void clear() {
        deleget.clear();
        keyMap.clear();
    }

    @Override
    public ReadWriteLock getReadWriteLock() { return null; }
    
    public void cycleKeyList(Object key) {
        keyMap.put(key, key);
        if (eldestKey != null) {
            delegate.removeObject(eldestKey);
            eldestKey = null;
        }
    }

}