MyBatis 캐시 기능 분석

MyBatis는 데이터베이스 쿼리 성능을 향상시키기 위해 캐시 기능을 제공합니다. 이 기능은 데이터의 일관성을 유지하기 위해 주의 깊게 관리해야 합니다.

MyBatis 캐시 관련 주요 개념

MyBatis 캐시: 데이터베이스 쿼리를 최적화하는 역할을 하지만, 오래된 데이터가 포함될 수 있습니다.

SqlSession: 데이터베이스와의 연결 세션을 나타내며, 데이터베이스에 대한 작업을 수행합니다.

MappedStatement: 데이터베이스에 전달할 명령어를 나타내며, SQL의 추상 표현으로 볼 수 있습니다.

Executor: 데이터베이스와 상호작용하는 실행자이며, MappedStatement를 매개변수로 받습니다.

namespace: Mapper 파일 내에서 하나만 설정 가능하며, Mapper 레벨에서 캐시 공유를 위한 식별자입니다.

매핑 인터페이스: 인터페이스를 정의하고, 해당 인터페이스 메서드는 SQL 작업을 나타냅니다. 실제 SQL 문장은 매핑 파일에 작성됩니다.

매핑 파일: XML 형식으로 작성되며, 여러 SQL 문장을 포함하고 있습니다. 일반적으로 각 단일 테이블에 하나씩 매핑됩니다.

MyBatis 1차 캐시

1차 캐시 원리

한 번의 SqlSession 동안 여러 번 동일한 쿼리가 실행되고, 중간에 삽입, 수정, 삭제가 없으면 두 번째 이후 쿼리는 캐시에서 결과를 가져옵니다.

각 SqlSession은 Executor를 가지고 있으며, Executor에는 LocalCache가 포함되어 있습니다. 쿼리가 실행되면 MappedStatement를 생성하고, LocalCache에서 결과를 검색합니다. 캐시가 존재하면 결과를 반환하고, 없으면 데이터베이스에서 조회 후 캐시에 저장합니다.

LocalCache는 해시맵 구조로 구성됩니다:

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

두 개의 SqlSession인 SqlSession1과 SqlSession2가 각각 자신의 캐시를 가지고 있으며, 캐시는 해시맵 구조로 이루어져 있습니다. 키는 Statement Id + Offset + Limit + Sql + Params로 구성되며, 값은 SQL 쿼리 결과입니다.

1차 캐시 설정

mybatis-config.xml 파일에서 localCacheScope 값을 설정하여 1차 캐시를 활성화하거나 비활성화할 수 있습니다.

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

  • SESSION: 1차 캐시 기능 활성화
  • STATEMENT: 현재 SQL 문장에만 적용됨

다음 예제 코드를 통해 1차 캐시 작동 방식을 확인해보겠습니다.

1차 캐시 테스트 사례

테스트 사례 (1): 1차 캐시만 활성화했을 때, 아래 코드에서 getStudentById() 메서드를 세 번 호출할 경우 올바른 설명은 무엇인가?

// SqlSession 열기
SqlSession sqlSession = factory.openSession(true);
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); 
// id=1로 학생 정보 조회
System.out.println(studentMapper.getStudentById(1)); 
// id=1로 학생 정보 조회
System.out.println(studentMapper.getStudentById(1)); 
// id=1로 학생 정보 조회
System.out.println(studentMapper.getStudentById(1));

정답: 첫 번째 쿼리는 데이터베이스에서 조회하고, 두 번째와 세 번째는 1차 캐시에서 조회합니다.

테스트 사례 (2): 1차 캐시만 활성화했을 때, 다음 코드에서 한 번의 쿼리 후 데이터를 수정하고 다시 쿼리할 경우, 두 번째 쿼리의 결과는 무엇인가?

// SqlSession 열기
SqlSession sqlSession = factory.openSession(true);
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); 
// id=1로 학생 정보 조회
System.out.println(studentMapper.getStudentById(1)); 
// 새로운 학생 추가
System.out.println("추가된 학생 수: " + studentMapper.addStudent(buildStudent()) + "명"); 
// id=1로 학생 정보 조회
System.out.println(studentMapper.getStudentById(1)); 
sqlSession.close();

정답: 첫 번째 쿼리는 데이터베이스에서 조회하고, 두 번째 쿼리는 데이터베이스에서 다시 조회합니다.

테스트 사례 (3): 1차 캐시가 활성화되었을 때, 두 개의 SqlSession이 생성되고, 첫 번째 SqlSession은 학생 A의 이름을 두 번 조회하고, 두 번째 SqlSession은 학생 A의 이름을 변경한 경우, 최종 결과는 무엇인가?

SqlSession sqlSession1 = factory.openSession(true); 
SqlSession sqlSession2 = factory.openSession(true); 
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class); 
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class); 
studentMapper2.updateStudentName("B",1); 
System.out.println(studentMapper.getStudentById(1)); 
System.out.println(studentMapper2.getStudentById(1));

정답:

A
B

설명: 1차 캐시는 SqlSession 단위로 고립되어 있으므로, sqlSession2에서 데이터를 수정하더라도 sqlSession1에서는 영향을 받지 않습니다.

1차 캐시가 작동하지 않는 경우

  1. 서로 다른 SqlSession
  2. 같은 SqlSession지만 쿼리 조건이 다름
  3. 같은 SqlSession에서 쿼리 사이에 삽입, 수정, 삭제가 발생함
  4. 캐시를 수동으로 초기화함

1차 캐시 요약

  • 1차 캐시는 간단한 해시맵 구조로, 기능적인 제한이 있음
  • SqlSession 내부에서만 사용 가능하며, 여러 SqlSession 또는 분산 환경에서는 데이터 불일치가 발생할 수 있음
  • 기본적으로 SESSION 레벨로 설정되어 있어, 한 세션 내 모든 쿼리가 공유함

MyBatis 2차 캐시

2차 캐시 개요

  • 2차 캐시는 여러 SqlSession 간의 데이터 공유를 가능하게 하며, namespace 단위로 설정 가능함
  • 캐시 기능이 유연하게 조절 가능함
  • 여러 테이블을 조인하는 쿼리 시, 데이터 불일치 문제가 발생할 수 있음
  • 분산 환경에서는 로컬 캐시 사용 시 데이터 불일치가 발생할 수 있으며, Redis나 Memcached 등 외부 캐시 시스템을 사용하는 것이 더 안전함

2차 캐시 원리

1차 캐시는 SqlSession 내부에서만 사용 가능하지만, 2차 캐시는 여러 SqlSession 간에 공유됩니다. 2차 캐시를 활성화하면 CachingExecutor가 Executor를 감싸고, 먼저 2차 캐시를 검색한 후, 없을 경우 1차 캐시를 검색합니다.

Namespace 단위로 2차 캐시가 공유됩니다. 다음과 같이 Mapper 파일에 namespace를 설정합니다.

<mapper namespace="mapper.StudentMapper"></mapper>

2차 캐시는 namespace 단위로 공유되며, 여러 SqlSession이 접근할 수 있습니다. 하지만, 여러 테이블을 조합한 쿼리에서는 다른 namespace의 데이터 변경이 감지되지 않아 데이터 불일치 문제가 발생할 수 있습니다.

2차 캐시 조회 순서

  1. 먼저 2차 캐시에서 검색
  2. 2차 캐시에 없으면 1차 캐시에서 검색
  3. 1차 캐시에도 없으면 데이터베이스에서 조회
  4. SqlSession 종료 후, 1차 캐시 데이터가 2차 캐시로 저장됨

2차 캐시 설정

mybatis-config.xml 파일에서 2차 캐시를 활성화할 수 있습니다.

<setting name="cacheEnabled" value="true"/>

2차 캐시 테스트 사례

update 연산이 특정 namespace의 2차 캐시를 갱신하는지 확인

SqlSession sqlSession1 = factory.openSession(true); 
SqlSession sqlSession2 = factory.openSession(true); 
SqlSession sqlSession3 = factory.openSession(true); 
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class); 
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class); 
StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class); 
System.out.println("studentMapper 읽기: " + studentMapper.getStudentById(1)); 
sqlSession1.commit(); 
System.out.println("studentMapper2 읽기: " + studentMapper2.getStudentById(1)); 
studentMapper3.updateStudentName("이사",1); 
sqlSession3.commit(); 
System.out.println("studentMapper2 읽기: " + studentMapper2.getStudentById(1));

정답:

장삼
장삼
이사

설명: 세 SqlSession은 2차 캐시를 공유하고 있으며, sqlSession3에서 데이터를 수정하면 namespace의 캐시가 갱신됩니다. sqlSession2는 마지막에 데이터베이스에서 조회합니다.

MyBatis 커스텀 캐시

커스텀 캐시 개요

MyBatis 2차 캐시가 필요 조건에 부합하지 않을 경우, 자체 캐시 구현이 가능합니다. 이는 org.apache.ibatis.cache.Cache 인터페이스를 구현해야 합니다.

EHCache 통합

EHCache는 MyBatis와 미리 통합되어 있으며, 직접 인터페이스를 구현할 필요가 없습니다. 의존성 추가 후, ehcache.xml 파일을 구성하고, Mapper 파일에서 캐시 타입을 지정합니다.

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

<?xml version="1.0" encoding="utf-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
    <!-- 디스크 저장 경로 -->
    <diskStore path="D:\passjava\ehcache"/>
    <defaultCache
            maxElementsInMemory="1000"
            maxElementsOnDisk="10000000"
            eternal="false"
            overflowToDisk="true"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
    </defaultCache>
</ehcache>

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

요약

본 문서에서는 MyBatis의 1차 캐시, 2차 캐시, 커스텀 캐시의 원리와 사용법을 살펴보았으며, 다양한 테스트 사례를 통해 캐시 기능을 검증했습니다. MyBatis 캐시 소스 코드 분석은 생략되었습니다.

태그: MyBatis Caching Database Optimization java SQL

8월 29일 15:31에 게시됨