본문 바로가기

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

[오픈소스] MyBatis 내장 cache에 대해서

MyBatis에는 2가지 내장 Cache가 존재합니다. local session cache, second level cache 두가지 입니다. local session cache는 임의로 켜거나 끌 수 없고, 무조건 활성화됩니다. 반면 second level cache는 mapper namespace 단위로 동작하여 개발자가 켜거나 끌 수 있습니다. Spring을 사용하지 않고 순수 MyBatis만을 사용해서 설명하겠습니다.



second level cache


먼저 설명하기 쉬운 second level cache부터 설명하도록 하겠습니다. 앞에서 언급했듯이 이 Cache는 mapper namespace 단위로 동작합니다. 이 Cache를 켜기 위해서는 mapper.xml 파일 안에 <cache />를 입력하면 됩니다.


Mapper에 <cache /> 설정을 안해주면 해당 Mapper에서 정의된 Statement는 캐싱이 되지 않습니다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mapper.TestMapper">
	<cache/>
	...
</mapper>

위의 mapper의 namespace는 "mapper.TestMapper"이며, 여기서 선언한 모든 SQL에 대해서는 Cache가 적용됩니다. 각 mapper에 설정한 cache 기능을 일괄적으로 disable 하고 싶으면, Global configuration에 다음과 같이 선언하면 됩니다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
	PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<settings>
		<setting name="cacheEnabled" value="false"/>
	</settings>
	<environments default="testdb">
		<environment id="testdb">
		<transactionManager type="JDBC" />
		<dataSource type="POOLED">
			<property name="driver" value="com.mysql.jdbc.Driver" />
			<property name="url" value="jdbc:mysql://localhot:3306/test?characterEncoding=utf8" />
			<property name="username" value="test" />
			<property name="password" value="test" />
		</dataSource>
	</environments>
	<mappers>
		<mapper resource="mybatis/test_mapper.xml" />
	</mappers>
</configuration>

위의 설정에서 "cacheEnabled" 값을 false로 설정하였습니다. default값은 true입니다. 위와 같이 false 설정을 하면 모든 mapper xml의 cache 선언이 무효화 됩니다.


Local session cache는 Session의 범위내에서 혹은 Statement 범위내에서 캐시사용 여부를 조정하는데, Second level cache는 Session 범위 밖에서의 캐시사용 여부를 조정합니다.


즉, Local cache는 Statement 실행 후, Commit이 되면 캐시를 지우는데 반해 Second level cache는 commit을 해도 캐시 데이터가 살아있습니다.



local session cache


출처: http://idea-sketch.tistory.com/30


local session cache는 MyBatis 공식 문서에서 구체적으로 설명하고 있지 않아서 아무 생각 없이 프로그램을 개발하면 엉뚱한 결과가 나올 수 있습니다. local session cache는 간단히 설명하자면 SqlSession 객체마다 갖고 있는 Cache입니다. 이 기능은 개발자가 임의로 끌 수 없습니다.


Local cache는 적용 범위만 조정가능한데 config 파일에 다음과 같이 작성하면 적용번위가 조정가능합니다.


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
	PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
	<settings>
		<setting name="localCacheScope" value="STATEMENT" /> <!-- STATEMENT / SESSION -->
	</settings>
</configuration>


session


- 기본적으로 적용되어 있는 설정으로 MyBatis를 사용하면서 특별한 옵션을 적용하지 않았다면 Local Cache의 적용범위는 SESSION으로 적용되어 있는 상태입니다.


- SESSION을 적용했을때 cache의 정보의 생존범위는 session이 유지되는 순간까지입니다. 즉 Transaction이 끝나거나(commit이나 rollback) 아니면 insert, update, delete가 길행되면 Local Cache가 보유하고 있던 cache 정보는 폐기됩니다.


 - autoCommit을 실행시켜 놓으면 쿼리를 실행할때마다 commit을 하기때문에 session에서 캐시가 유지되는지 확인하기 힘듭니다.



[1]번째 시도의 A번째 시도에서 데이터를 찾는데 시간(1820ms)가 소모되었지만 캐시를 통해 데이터를 찾을때는 cahce 정보를 사용하기에 시간(0~1ms)을 거의 거의 소모하지 않았습니다.



statement


 - Config 설정에서 캐싱 범위를 STATEMENT로 하면 cache 정보의 생존범위는 Statement입니다.

 - mapper에 정의된 액션 하나당 하나의 statement로 추측합니다. 때문에 Config를 STATEMENT로 바꾸면 SESSION과 동일한 코드를 한다면 다음과 같은 결과가 나옵니다.


 - SESSION과는 다르게 commit이 되지 않았지만 [1]번째의 A,B,C번째 시도에 각각 데이터를 찾는데 시간이 소모됩니다. 즉 statement 하나가 끝나면 바로 캐시 데이터를 폐기하고 다음번에 데이터를 찾을때 다시금 새로운 데이터를 찾는데 시간을 소모합니다.


 - Local cache 범위를 statement로 설정하고 일반적으로 Mapper를 구성해서 사용하면 거의 cache를 사용하지 않는 것과 비슷한 효과를 낼 것이라고 추측합니다.





기본적으로는 다음 조건을 만족할 때 Cache가 flush 됩니다.


 - 명시적인 SqlSession 객체에 대한 commit(), rollback(), close() 호출(auto commit에 의한 commit은 해당 안됨)

 - SqlSession.clearCache() 호출



이 flush 조건만으로는 충분하지 않은 경우가 있습니다. 설명의 편의를 위해서 다음 Table을 갖고 설명하도록 하겠습니다.

create table session_test (
	col1 varchar(10) primary key,
	col2 int
);


이 테이블에서 col1은 primary key이고, col2는 count 값을 갖고 있는 컬럼입니다. 이 테이블 관리를 위해서 다음과 같은 순서로 SQL을 처리한다고 가정해보겠습니다.


1) 레코드 생성 -> 2) col2의 값을 1만큼 증가 -> 3) col2 값을 조회


package test;

import java.io.Reader;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import mapper.TestMapper;
import domain.TestTable;

public class TestRun {
	private SqlSessionFactory sqlSessionFactory;

	public static void main(String[] args) thrwos Exception {
		TestRun main_class = new TestRun();
		main_class.initMyBatis();
		main_class.runQuery();
	} // End of main()

	private void initMybatis() throws Exception {
		String resource = "mybatis/configuration.xml";
		Reader reader = null;
		reader = Resources.getResourceAsReader(resource);
		this.sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
	} // End of initMybatis()

	private void runQuery() throws Exception {
		SqlSession session = null;
		session = this.sqlSessionFactory.openSession(true);
		// Insert
		TestMapper mapper = session.getMapper(TestMapper.class);
		TestTable data = new TestTable();
		data.col1 = "key1";
		data.col2 = 1;
		mapper.insertTestTable(data);
		// Select
		int col2 = mapper.selectTestTable("key1");
		System.out.println("col2 value : " + col2);
		// Update
		UpdateApp updateApp = new UpdateApp(this.sqlSessionFactory);
		updateApp.updateData("key1");
		// Select again
		col2 = mapper.selectTestTable("key1");
		System.out.println("col2 value : " + col2);

		session.close();
	} // End of runQuery()
	
} // End of class TestRun


위와 같이 개발한 후 TestRun.java를 실행합니다. 기대하는 결과값은 두번째 col2 value 값이 2임을 예상할 수 있으나, 실제 실행결과는 col2 value가 1로 나타나는 것을 확인할 수 있다. 분명 DB에 조회해보면 2로 정상적으로 업데이트 되었음을 알 수 있습니다. 그러나 위와 같이 이상한 값이 나오는 이유는 두번째 Query에서 SqlSession 객체 내부 Cache를 이용하기 때문입니다. 따라서 이 경우 2번째 Query에서 DB에 조회하기 위해서는 다음 2가지 방법이 있습니다.


  1) SqlSession.clearCache()를 호출

  2) SqlMapper 선언에 useCache="false" flushCache="true" 설정 (<select>에 대한 default는 useCache="true", flushCache="false")


위의 2가지 방법 중 하나를 선택하면 원하는 결과값을 얻을 수 있습니다. 위의 샘플 프로그램 예제는 상태 count를 관리하는 프로그램에서 구현하는 전형적인 유형입니다. 이 경우 Mybatis local session cache의 특징을 정확히 이해해야 버그없는 프로그램을 개발할 수 있습니다.

	// Insert
	Test mapper = session.getMapper(TestMapper.class);
	TestTable data = new TestTable();
	data.col1 = "key1";
	data.col2 = 1;
	mapper.insertTestTable(data);
	// Select
	int col2 = mapper.selectTestTable("key1");
	System.out.println("col2 value : " + col2);
	// Update
	UpdateApp updateApp = new UpdateApp(this.sqlSessionFactory);
	updateApp.updateData("key1");
	// Cahce clear
	session.clearCache();
	// Select again
	col2 = mapper.selectTestTable("key1");
	System.out.println("col2 value : " + col2);
	
	session.close();
<select id="selectTestTable"
	parameterType="String"
	resultType="int"
	useCache="false" flushCache="true">
	select col2
	from session_test
	where col1 = #{col1}
</select>


출처: http://blog.naver.com/PostView.nhn?blogId=tkstone&logNo=50193992290