Cache

Hibernate을 사용하면 입력 인자로 전달된 객체를 정의된 테이블로 매핑시켜 데이터 액세스 처리를 수행해야 하는데 Hibernate에서는 이로 인해 발생 가능한 성능 이슈를 개선하기 위해 Cache를 활용한다. 특히, 어플리케이션의 조회 기능이 전체 실행 시간의 많은 비중을 차지하는 경우 매번 DB에 접근하지 않고 Cache에 저장된 객체를 사용함으로써 성능을 향상시킬 수 있게 되는 것이다.

1LC (1 Level Cache)

Hibernate Session 내부에 정의된 Cache로, Session의 시작과 종료 사이에서 사용되며 한 Session 내에서 Hibernate을 통해 읽혀진 객체들을 보관하는 역할을 수행한다. Hibernate은 하나의 Session 내에서 동일한 객체를 한 번 이상 Loading할 경우 2번째부터는 1LC로부터 해당 객체를 추출하고 또한, 한 Session 범위 내에서 객체의 속성 변경시 변경 사항은 Session 종료시에 자동적으로 DB에 반영하도록 한다. 즉, 하나의 Hibernate Session 내에서 동일한 객체에 대한 재조회가 이루어지는 경우 1LC를 이용함으로써 DB 접근 횟수를 줄여주기 때문에 어플리케이션 성능 향상에 도움이 되는 것이다. 1LC는 Hibernate에서 기본적으로 제공하는 Cache이므로 별도의 설정없이도 적용된다.
public void testFindMovie() throws Exception {
	newSession();
	// Add data to DB
	SetUpInitData.initializeData(session);	
	// 2. find a movie without accessing DB (using 1LC)
	/* #1 */ Movie movie = (Movie) session.get(Movie.class, "MV-00001");
	
	Set categories = movie.getCategories();
	categories.iterator();
	
	// 3. find a movie again without accessing DB (using 1LC)
	movie = (Movie) session.get(Movie.class, "MV-00001");
	
	categories = movie.getCategories();
	categories.iterator();
	closeSession();
}
				
위와 같이 작성할 경우 동일한 Session 내에서 SetUpInitData.initializeData(session)를 통해 save된 Persistence 객체는 1LC에 저장되므로 다음에 #1번 코드에서처럼 동일한 Persistence 객체 조회시 DB에 재접근하지 않고도, Cache를 통해 조회된다. testFindMovie() 메소드를 포함한 HibernateFirstLevelCacheTest.java 테스트 소스를 DEBUG 모드로 실행시켜서 실행되는 쿼리를 콘솔창을 통해 확인해 보면 이를 확인할 수 있을 것이다. SetUpInitData.java에 대한 내용은 여기 에서 확인할 수 있다.

2LC (2 Level Cache)

2LC는 어플리케이션 단위의 Cache로, 어플리케이션 관점에서의 Cache 기능을 지원한다. 이는 여러 트랜잭션들을 통해 Load된 Persistence 객체를 Session Factory 레벨에서 저장하는 방법으로 처리된다.
Hibernate 속성 정의 파일 내에 hibernate.cache.use_second_level_cache, hibernate.cache.provider_class 등을 정의 하고, 2LC에 저장되어야 할 Persistence Class 매핑 파일의 <cache> 속성을 정의하면 해당 어플리케이션을 구성하는 특정 Persistence 객체들에 대해 2LC를 적용할 수 있다.
다음은 2LC에 대한 속성이 정의되어 있는hibernate.cfg.xml 파일의 일부이다.
<property name="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</property>
<property name="hibernate.cache.use_second_level_cache">true</property>
				
다음은 cache 속성이 read-write로 설정되어 있는 Persistence Class 매핑 파일 Country.hbm.xml 의 일부이다.
<class name="anyframe.sample.model.bidirection.Country" table="COUNTRY" lazy="true" schema="PUBLIC">
	<cache usage="read-write"/>
	<id name="countryCode" type="string">
	    <column name="COUNTRY_CODE" length="12" />
	<generator class="assigned" />
	</id>
	<property name="countryId" type="string">
	    <column name="COUNTRY_ID" length="2" not-null="true" />
	</property>
	...
 </class>
 				
cache의 속성은 위에서 언급한 read-write외에도 다음과 같은 속성값으로 정의할 수 있다.
  • read-only : Persistence 객체가 변경되지 않는 경우에 사용 가능하다. 수정이 없으므로 분산 환경에서도 안전하게 사용 가능하며 가장 빠른 성능을 제공한다.
  • read-write : Persistence 객체가 변경되는 경우에 사용 가능하다. DBMS의 read-committed와 동일하게 동시 접근을 관리한다.
  • nonstrict-read-write : 트랜잭션 격리를 엄격히 적용할 필요가 없는 경우 사용 가능하다.
  • transactional : 완전한 트랜잭션을 보장하나 가장 느린 성능을 제공한다. JTA 환경 내에서만 사용된다.
위와 같은 설정을 기반으로 HibernateSecondLevelCacheTest.java testFindCountry() 메소드를 실행해보면 다음의 #1번 코드에 의해 새로운 Session이 시작되었음에도 #2번 코드에서 DB에 접근하지 않고 이전 Session에서 Cache에 저장한 값을 가지고 사용한다는 것을 확인할 수 있다.
public void testFindCountry() throws Exception {
	newSession();
	SetUpInitData.initializeData(session);
	closeSession();
	
	// 2. find a movie without accessing DB (using 2LC)
	/* #1 */ newSession();
	/* #2 */ Country country = (Country) session.get(Country.class, "COUNTRY-0001");
	
	Set movies = country.getMovies();
	movies.iterator();
	closeSession();
	
	// 3. find a movie again without accessing DB (using 2LC)
	newSession();
	country = (Country) session.get(Country.class, "COUNTRY-0001");
	
	movies = country.getMovies();
	movies.iterator();
	closeSession();
}
				
DEBUG 모드에서 테스트케이스를 실행시켜보면서 DB에 접근하지 않고도 2LC를 통해 객체가 조회되는 것을 살펴볼 것을 권장한다. HibernateSecondLevelCacheTest.java 의 testFindMovie()는 2LC 사용하지 않는 Persistence Class인 Movie에 대한 테스트로써 앞서 언급한 testFindCountry()와 달리 Session이 다를 경우 매번 DB에 접근하여 해당 Persistence 객체를 조회해오는 것을 알 수 있다.

단, 2LC를 적용하고자 할 경우 해당 어플리케이션을 통하지 않고, 외부에서 직접적으로 DB 정보가 수정될 가능성이 있다면 데이터의 동기화를 위해 세밀한 Cache 속성 제어가 필요함에 유의하도록 한다.

분산 Cache

하나의 어플리케이션을 대상으로 하는 경우 앞서 언급한 2LC를 사용하는데 문제가 없으나, 일반적인 Clustered 환경에서 실행된 여러 개의 어플리케이션에 속한 2LC 사이의 데이터 동기화는 중요한 사항이 될 것이다. 이를 위해 Hibernate는 분산 Cache를 지원하는 구현체를 통해 분산 어플리케이션에 대한 Cache 기능을 지원한다.

다음에서는 분산 Cache를 지원하는 구현체별로 설정 방법 및 실행 결과에 대해 살펴보기로 하자.

OSCacheProvider 이용

OSCache 2.0부터 분산 Cache를 지원한다. 현재 OSCache는 분산된 Cache들이 Caching하고 있는 데이터 동기화를 위해 JavaGroups 또는 JMS를 통해 Event를 처리할 수 있도록 구현체를 제공한다. 단, 분산 Cache 사이에서 flush Event 발생시(Caching된 객체를 Cache에서 지울때)에만 Message를 broadcast하는 기능이 지원된다.

본 페이지에서는 OSCacheProvider와 JMS 기능을 제공하는 오픈소스 ActiveMQ를 사용하여 분산 Cache를 관리하는 방법에 대해 알아볼 것이다. 다음은 Hibernate Configuration 파일로, hibernate.cache.provider_class 속성값으로 OSCacheProvider를 지정하고 있음을 알 수 있다.
<session-factory>
	<!-- 중략 -->    	
	<property name="hibernate.format_sql">true</property>
	<property name="hbm2ddl.auto">create</property>
	<property name="hibernate.cache.use_second_level_cache">true</property>
 	<property name="hibernate.cache.provider_class">
 	    com.opensymphony.oscache.hibernate.OSCacheProvider</property>
	<property name="hibernate.cache.region_prefix">
	    hibernate.cache</property> 
	<property name="com.opensymphony.oscache.configurationResourceName">
	    oscache-hibernate.properties</property>
	<!-- 중략 -->
</session-factory>				
				
또한, Hibernate Cache 영역에 대해 hibernate.cache.region_prefix 를 별도로 지정하였다. (hibernate.cache.region_prefix가 위와 같이 정의된 경우, Persistence Class인 anyframe.sample.model.bidirection.Country는 해당 2LC의 hibernate.cache.anyframe.sample.model.bidirection.Country 영역에 객체가 Caching된다.) 끝으로, com.opensymphony.oscache.configurationResourceName 속성에 OSCacheProvider가 분산 Cache들 사이의 데이터 동기화를 위해 필요로 하는 모든 속성 정보를 정의해 주어야 한다.

위에서 com.opensymphony.oscache.configurationResourceName의 속성값으로 정의한 oscache-hibernate.properties 파일 내용은 다음과 같다.
cache.event.listeners=anyframe.core.cache.impl.JMSBroadcastingListener
cache.cluster.jndi.config=jndi.properties

cache.cluster.jms.topic.factory=TopicConnectionFactory
cache.cluster.jms.topic.name=dynamicTopics/topic
cache.cluster.jms.node.name=node1
				
각 속성은 다음과 같은 의미를 지닌다.
  • cache.event.listeners : 한 Cache에 변경 사항이 발생한 경우 분산 Cache간 동기화를 위해 Event 처리가 필요하며, OSCache에서는 JMS를 통해 Event를 처리하기 위해 기본적으로 com.opensymphony.oscache.plugins.clustersupport.JMSBroadcastingListener를 제공한다. 그러나 이것은 앞서 언급했듯이 flush Event 발생시에만 Message를 broadcast하는 기능만 지원되므로 Caching된 객체에 대해 수정이 발생한 경우에는 Message Broadcasting되지 않는 취약점이 있다. 따라서, Anyframe에서는 이를 보완한 별도 Cache Event Listener 클래스(anyframe.core.cache.impl.JMSBroadcastingListener)를 제공 하고 있다. Anyframe의 JMSBroadcastingListener는 특정 어플리케이션을 통해 Caching된 객체에 수정이 발생한 경우 Clustering된 모든 어플리케이션의 Cache에서 해당 객체를 지우도록 Event를 보낸다.


  • cache.cluster.jndi.config : Cache Event Listener에서 JMS Server에 접근하기 위해 필요한 환경 정보를 정의하기 위한 파일이다. JMS Server의 InitialContextFactory 클래스를 정의하기 위한 java.naming.factory.initial 와 Provider URL 정의를 위한 java.naming.provider.url 를 정의해준다. 다음은 jndi.properties 파일 내용이다.
    java.naming.factory.initial=org.apache.activemq.jndi.ActiveMQInitialContextFactory
    java.naming.provider.url=tcp://localhost:61616					
    					
  • cache.cluster.jms.topic.factory : JMS topic connection factory에 접근하기 위한 JNDI명을 정의한다. ActiveMQ의 경우, TopicConnectionFactory와 같이 정의하면 Topic을 사용하여 Messaging 처리를 수행하게 된다.


  • cache.cluster.jms.topic.name : OSCache에서 Message를 보내기 위해 사용할 Topic의 JNDI명을 정의한다. ActiveMQ의 경우, dynamicTopics/ 다음에 ActiveMQ에 생성한 Topic명을 정의해 주는데 만일 정의한 이름을 가진 Topic이 존재하지 않으면, 해당 Topic이 신규로 생성된다.


  • cache.cluster.jms.node.name : 분산 환경을 구성하는 여러 어플리케이션 중 해당 어플리케이션을 식별하기 위한 식별자를 정의한다. 분산 환경을 구성하고 있는 각 어플리케이션들은 모두 다른 node명을 갖도록 지정해야 한다. Cache Event가 발생할 때 해당 어플리케이션을 포함하여 분산 환경을 구성하고 있는 모든 어플리케이션의 Cache에 해당 Event가 Send되는데, Cache Event가 발생한 해당 어플리케이션에서는 Event를 받더라도 아무런 Action을 취할 필요가 없다. 따라서, cache.cluster.jms.node.name 는 Cache Event가 어느 어플리케이션에서 발생했는지 알 수 있는 정보로 활용된다.
위와 같은 설정이 모두 완료되었다면, 동일한 어플리케이션 2개를 각기 다른 WAS를 통해 시작시킨 후 다음과 같은 유형의 요청 수행시 Cache가 제대로 동작하는지 확인해보자. 이때, 사용하는 JMS 서버 라이브러리와 함께 jms spec., j2ee management spec. 라이브러리를 각 어플리케이션에 배포하여 구동시켜줘야 한다. 이 예제에서는 JMS 서버로 ActiveMQ를 사용하므로, 각 어플리케이션의 WEB-INF/lib 폴더에 activemq-core-x.x.x.jar와 jms spec jar, j2ee management spec jar 파일을 배포하여 테스트하였다.
  • 어플리케이션 A에서 특정 데이터 조회 후, 어플리케이션 B에서 동일한 데이터 조회시 별도 쿼리문 수행없이 해당 데이터가 조회되는지 확인한다. 즉, 어플리케이션 B는 DB에 접근하지 않고 Cache를 통해 데이터를 조회하는지 확인한다.
  • 어플리케이션 A에서 특정 데이터 수정 후, Event가 Send되고 어플케이션 B에서 해당 Event를 Receive하는지 확인한다.


    또한 어플리케이션 B에서 어플리케이션 A를 통해 수정한 데이터 조회시 해당 객체는 Cache Event Listener에 의해 Cache로부터 지워졌으므로, 기존에 Caching된 객체를 그대로 읽지 않고, DB에 접근하여 변경된 데이터를 읽어오는지 확인한다.

(* WAS가 Tomcat일 경우 spring-tomcat-weaver.jar 파일을 다운로드하여 Tomcat 설치 폴더\server\lib에 복사해야 한다.)

Resources

  • 다운로드
  • 샘플 테스트 코드를 포함하고 있는 anyframe-hibernatetest-src.zip 파일을 다운받은 후, 테스트 환경 설정 을 참조하여 위에서 제시한 예제 코드 (src/test/java 폴더의 anyframe.core.hibernate.cache 패키지에 속한 *Test.java)를 실행해 볼 수 있다.
    Name
    Download
    anyframe-hibernatetest-src.zip
    Download