회사에서 사용자의 이용 현황 통계를 추출해야하는 과제가 있었습니다.
여러 테이블에서 사용자의 이용 현황을 조회해 csv 파일에 write하는 간단한 작업이었습니다.
한참 배치가 잘 돌다가 생각치도 못한 문제가 발생했습니다.
OutOfMemoryError?!
통계 Batch 서버에 아래와 같이 Out of Memory Error 가 발생하며 사용자 데이터 집계에 실패하는 일이 발생했습니다.
java.lang.OutOfMemoryError: GC overhead limit exceeded
경력은 (매우) 짧지만, Java 개발자로 일하면서 OOM 에러는 거의 처음 보는 것 같았습니다.
힙 덤프파일을 떠서, Eclipse MAT를 사용하여 분석해보니, MyBatis에서 점유하고 있는 heap 크기가 상당하다는 것을 발견했습니다.
MyBatis의 캐시 정책
MyBatis에는 2가지 내장 캐시가 존재합니다.
mybatis 공식 홈페이지에는 local 캐시에 대한 구체적인 정보가 존재하지 않아 검색을 통해 얻은 정보는 다음과 같습니다.
- local 캐시(Level 1 cache)
- 저장 범위가 session 또는 statement이며, 디폴트는 session이다. 기본적으로 작동하며, 해제가 불가하다.
- 2nd 레벨 캐시
- 저장 범위가 namespace이며, 디폴트로 작동하지 않기에 필요시<cache/> 구문을 추가해 주어야 한다.
ref. techblog.lotteon
local 캐시에 대해 간단히 설명하자면 SqlSession 객체마다 갖고 있는 Cache입니다. 이 기능은 개발자가 임의로 끌 수 없습니다.
다만, 적용 범위를 session(Default)에서 statement로 축소할 수 있습니다.
이슈 분석 및 해결
저의 경우, MyBatis의 local 캐시(Level 1 cache)에 대해 별다른 설정을 하고 있지 않았기 때문에 디폴트 값인 session 범위에서 로컬 캐싱이 발생하고 있었습니다.
로컬 캐시가 어떻게 동작하며, 언제 지워지는지 확인하기 위해 코드를 확인해봤습니다.
SqlSessionTemplate을 통해 쿼리를 실행할 경우, BaseExecutor:query()를 사용하게 됩니다.
위 코드를 참고해보면, 쿼리가 실행될 때 아래의 두 가지 경우에 로컬 캐시를 지워주는 것을 확인할 수 있습니다.
// 1. MappedStatement 인스턴스의 isFlushCacheRequired()가 true일 때
if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
this.clearLocalCache();
}
... 중략...
// 2. Local Cache Scope이 Statement 일 때
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
this.clearLocalCache();
}
query() 메소드 외에도 같은 클래스 내부에 있는 commit(), rollback(), update() 메소드에서도 로컬 캐시를 지워주고 있습니다.
(코드는 아래 더보기 란에)
# BaseExecutor.java



위 내용을 다시 정리해보면, Local 캐시가 비워지는 경우는 아래와 같습니다..
1. MappedStatement 인스턴스의 isFlushCacheRequired()가 true일 때
2. Local Cache Scope이 Statement 일 때
3. commit(), rollback(), update() 메소드가 호출 될 때
SqlSession에서 insert문, update문, delete문 질의 시 위 update()를 호출하는데, 저의 경우 DB에서 select문만 질의하고 파일에만 write 하다보니, update()가 호출이 되지 않아 캐시가 지워지지 않았던 것이었습니다.
이 문제를 해결하기 위해, 위에 언급한 1번, 2번 경우 중 하나를 적용해서 캐시가 비워질 수 있게 하면 됩니다.
아래는 각각 적용 방법입니다.
1번 방식
MappedStatement 인스턴스의 isFlushCacheRequired()가 true로 나올 수 있게 아래와 같이 SQL 구문을 수정한다.
<select id="{selectId}" parameterType="{parameterType}" resultType="{resultType}" flushCache="true">
2번 방식
Local Cache Scope을 Statement 로 수정한다.
<setting name="localCacheScope" value="STATEMENT"/>
<settings>
<setting name="cacheEnabled" value="false" />
<!-- localCacheScope을 Statement로 수정 -->
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
참고
https://techblog.lotteon.com/%EC%96%B4%EB%9E%8F-%EC%97%AC%EA%B8%B0%EC%97%90%EC%84%9C-oom-%EB%B0%9C%EC%83%9D%ED%95%A0-%EC%A4%84%EC%9D%B4%EC%95%BC-503ddf286fd
https://mybatis.org/mybatis-3/sqlmap-xml.html#cache
https://moi.vonos.net/java/mybatis-caching/