트랜잭션은 ASID를 보장해야 한다.
트랜잭션간의 격리성을 완벽히 보장하려면 트랜재견을 거의 차례대로 실행해야 하는데 이러면 동시성 처리 성능이 매우 나빠진다.
ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다.
(READ UNCOMMITED 격리 수준이 가장 낮고 SERIALIZABLE 가장 높음)
격리 수준이 낮을수록 동시성이 증가하지만 다양한 문제가 발생한다.

READ UNCOMMITTED : DIRTY READ를 허용하는 격리 수준. 즉, 커밋하지 않은 데이터를 읽을 수 있다.
트랜잭션1이 수정하고 있는 중 커밋하지 않아도 트랜잭션 2가 수정중인 데이터를 읽을 수 있다. 이를  DIRTY READ라고 한다.
또한 트랜잭션 2가 DIRTY READ한 데이터를 사용하는데 트랜잭션 1이 롤백하면 데이터 정합성 문제가 발생할 수 있다.
READ COMMITTED : DIRTY READ를 허용하지 않지만 NON-REPEATABLE READ 허용하는 격리 수준.
즉, 커밋한 데이터만 읽을 수 있다.
DIRTY READ가 발생하지 않음
예를 들어 트랜잭션 1이 회원 A를 조회 중인데 갑자기 트랜잭션 2가 회원 A를 수정하고 커밋하면 트랜잭션 1이 다시 회원 A를 조회했을 때 수정된 데이터가 조회되는데 이처럼 반복해서 같은 데이터를 읽을 수 없는 상태를 NON-REPEATABLE READ 라고 한다.
REPEATABLE READ : NON-REPEATABLE READ를 허용하지 않지만 PHANTOM READ는 허용하는 격리 수준.
즉, 한번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다.
다만 데이터가 추가되면 이는 반영돼서 조회된다.
예를 들어 트랜잭션 1이 10살 이하의 회원을 조회했는데 트랜잭션 2가 5살 회원을 추가하고 커밋하면 트랜잭션1이 다시 10살 이하의 회원을 조회했을 때 회원 하나가 추가된 상태로 조회되는데 이처럼 반복 조회시 결과 집합이 달라지는 것을 PHANTOM READ라고 한다. 
SERIALIZABLE : 가장 엄격한 트랜잭션 격리 수준.
PHANTOM READ 발생하지 않지만 동시성 처리 성능이 급격히 떨어진다.
애플리케이션 대부분은 동시성 처리가 중요하므로 데이터베이스들은 보통 READ COMMITTED 격릴 수준을 기본으로 사용한다. 일부 중요한 비즈니스 로직에 더 높은 격리 수준이 필요하면 데이터베이스 트랜잭션이 제공하는 잠금 기능을 사용하면 된다.
참고 : 트랜잭션 격리 수준에 따른 동작 방식은 데이터베이스마다 다르다. 최근에는 더 많은 동시성 처리를 위해 락보다는 MVCC를 사용한다고 한다.
JPA의 영속성 컨텍스트(1차 캐시)를 적절히 활용하면 데이터베이스 트랜잭션이 READ COMMITED 격리 수준이어도 애플리케이션 레벨에서 반복 가능한 읽기(REPEATABLE READ)가 가능하다.
물론 엔티티가 아닌 스칼라 값을 직접 조회하면 영속성 컨텍스트가 관리하지 않기 때문에 반복 가능한 읽기를 할 수 없다.
JPA는 데이터베이스 격리 수준을 READ COMMTIED로 가정한다. 만약 일부 로직에 더 높은 격리 수준이 필요하면 낙관적 락과 비관적 락 중 하나를 사용하면 된다.
데이터베이스가 제공하는 락 기능이 아닌 JPA가 제공하는 버전 관리 기능을 사용한다. 즉, 애플리케이션이 제공하는 락이다.
특징 : 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다.
데이터베이스가 제공하는 락 기능을 사용한다.
ex) select for update 구문
사용자 1,2가 동시에 게시물의 제목을 수정한다고 할 때 1이 먼저 완료를 하고 2가 나중에 완료를 한 경우 2로 덮어지는 경우가 발생한다. 이것을 두 번의 갱실 분실 문제라고 한다.
해당 문제는 데이터베이스 트랜잭션의 범위를 넘어서 트랜잭션으로 해결할 수 없다. 이를 해결하는 방법으로 다음 3가지가 있다.
JPA가 제공하는 낙관적 락을 사용하려면 @Version을 사용해서 버전 관리 기능을 추가해야 한다.
@Entity
public class Board {
		@Id
		private String id;
		private String title;
		@Version
		private Integer version;
}
버전 관리 기능을 사용하려면 위처럼 버전 관리용 필드를 생성하고 @Version을 붙이면 된다. 이러면 엔티티를 수정할 때마다 버전이 하나씩 자동으로 증가한다.
엔티티를 수정할 때 조회 시점의 버전과 수정 시점의 버전이 다르면 예외가 발생하여 최초 커밋만 인정하기가 적용된다.
예를 보자.

//트랜잭션1 조회 title="제목A", version=1
Board board = em.find(Board.class, id);
//트랜잭션 2에서 해당 게시물을 수정해서 title="제목C", version=2로 증가
board.setTitle("제목B"); //트랜잭션 1 데이터 수정
save(board);
tx.commit(); //예외 발생, 데이터베이스 version=2 엔티티 version=1
트랜잭션1이 제목을 바꾸려고 조회를 했다. 그런데 트랜잭션 2에서 조회를 한 다음 제목을 수정하고 커밋해서 version이 수정되었다. 이러면 트랜잭션1이 제목을 변경하고 커밋하려고 하면 version이 달라 예외가 발생한다.
엔티티를 수정하고 트랜잭션을 커밋하면 영속성 컨텍스트를 플러시 하면서 UPDATE 쿼리를 실행한다. 이때 버전을 사용하는 엔티티면 검색 조건에 엔티티의 버전 정보를 추가한다.
UPDATE BOARD
SET
		TITLE=?
		VERSION=? (버전 + 1 증가)
WHERE
		ID=?
		AND VERSION=? (버전비교)
이처럼 version을 비교하고 엔티티 버전이 같으면 수정을 진행하고 버전의 값을 하나 올린다. 데이터베이스에 버전이 증가해서 수정 중인 엔티티와 버전이 다를 경우에는 수정할 대상이 없다. 이럴 경우 버전이 이미 증가한 것으로 보고 JPA가 예외를 발생시킨다.
버전은 엔티티의 값을 변경하면 증가한다. 값 타입인 임베디드 타입과 값 타입 컬렉션은 논리적인 개념상 해당 엔티티의 값이므로 수정하면 엔티티의 버전이 증가한다.
단 연관관계 필드는 외래 키를 관리하는 연관관계의 주인 필드를 수정할 때만 버전이 증가한다.
@Version은 JPA가 관리하므로 개발자가 벌크 연산을 제외하고는 임의로 수정하면 안 된다.
참고 : 벌크 연산은 버전을 무시한다. 벌크 연산에서 버전을 증가하려면 버전 필드를 강제로 증가시켜야 함
JPA를 사용할 때 추천하는 전략은 READ COMMITED + 낙관적 버전 관리(두 번의 갱신 내역 분실 문제 예방)이다.
Board board = em.find(Board.class, id, LockModeType.OPTIMISTIC);
Board board = em.find(Board.class, id);
...
em.lock(board, LockModeType.OPTIMISTIC);
JPA가 제공하는 락 옵션은 아래와 같다. (javax.persistence.LockModeType)

JPA가 제공하는 낙관적 락은 버전(@Version)을 사용한다. 낙관적 락은 트랜잭션을 커밋하는 시점에 충돌을 알 수 있다.
낙관적락은 옵션 없이 @Version으로만으로 적용된다.
용도: 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경(삭제)되지 않아야 하고, 조회 시점부터 수정 시점까지를 보장
동작: 엔티티를 수정할 때 버전을 체크하면서 버전을 증가(UPDATE 쿼리 사용). 이때 DB의 버전 값이 현재 버전이 아니면 예외가 발생
이점: 두 번의 갱신 분실 문제를 예방
@Version만 사용하면 엔티티가 수정해야 버전을 체크하지만 OPTIMISTIC 옵션을 추가하면 엔티티를 조회만 해도 버전을 체크한다.
용도: 조회한 엔티티는 트랜잭션이 끝날 때까지 다른 트랜잭션에 의해 변경되지 않아야 함. 조회 시점부터 트랜잭션이 끝날 때까지 조회한 엔티티가 변경되지 않음을 보장
동작 : 트랜잭션을 커밋할 때 버전 정보를 조회해서(SELECT 쿼리 사용) 현재 엔티티의 버전과 같은지 검증하고 만약 같지 않으면 예외가 발생
이점 : OPTIMISTIC 옵션은 DIRTY READ와 NON-REPEATABLE READ를 방지
용도 : 논리적인 단위의 엔티티 묶음을 관리할 수 있다. 예를 들어 게시물과 첨부파일이 일대다, 다대일의 양방향 연관관계이고 첨부파일이 연관관계의 주인이다. 게시물을 수정하는데 단순히 첨부파일만 추가하면 게시물의 버전은 증가하지 않는다. 해당 게시물은 물리적으로 변경되지 않았지만, 논리적으로는 변경되었다. 이때 게시물의 버전도 강제로 증가하려면 이 옵션을 사용하면 된다.
동작 : 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해서 버전 정보를 강제로 증가시킨다. 이때 데이터베이스의 버전이 엔티티의 버전과 다르면 예외가 발생한다. 추가로 엔티티를 수정하면 수정 시 버전 UPDATE가 발생한다. 따라서 총 2번의 버전 증가가 나타날 수 있다.
이점 : 강제로 버전을 증가해서 논리적인 단위의 엔티티 묶음을 버전 관리할 수 있다.
JPA가 제공하는 비관적 락은 데이터베이스 트랜잭션 락 메커니즘에 의존한다.
주로 SQL 쿼리에 select for update 구문을 사용하면서 시작하고 버전 정보는 사용하지 않는다.
비관적 락ㅇ느 주로 PESSIMISTIC_WIRTE 모드를 사용한다.
비관적 락의 일반적 옵션. 데이터베이스에 쓰기 락을 걸때 사용한다.
용도 : 데이터베이스에 쓰기 락을 건다.
동작 : 데이터베이스 select for update를 사용해서 락을 건다.
이점 : NON-REPEATABLE READ를 방지한다. 락이 걸린 로우는 다른 트랜잭션이 수정할 수 없다.
데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용한다.(일반적으로 잘 사용하지 않음)
데이터베이스 방언에 의해 PESSIMISTIC_WRITE로 동작한다.
비관적 락중 유일하게 버전 정보를 사용하고 비관적 락이지만 버전 정보를 강제로 증가시킨다.
하이버네이트는 nowait를 지원하는 데이터베이스에 대해서 for update nowait 옵션을 적용한다.
비관적 락을 사용하면 락을 획득할 때까지 트랜잭션이 대기하는데 무한정 기다릴 수 없으므로 타임아웃 시간을 줄 수 있다.
타임아웃은 데이터베이스 특성에 따라 동작하지 않을 수 있다.
네트워클 통해 데이터베이스에 접근하는 시간 비용은 애플리케이션 서버에서 내부 메모리에 접근하는 시간비용보다 수만에서 수십만 배 비싸다.
영속성 컨텍스트 내부에 1차 캐시를 이용해서 시간을 줄일 수 있지만 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다. OSIV를 이용해도 클라이언트 요청이 들어오고 끝날 때까지만 1차 캐시가 유효하다. 따라서 애플리케이셔 전체로 보면 데이터베이스 접근 횟수를 획기적으로 줄이지는 못한다.
하이버네이트를 포함한 대부분의 JPA 구현체들은 애플리케이션 범위의 캐시를 지원하는데 이것을 공유 캐시 또는 2차 캐시라 한다. 이런 2차 캐시를 이요하면 애플리케이션 조회 성능을 향상할 수 있다.
1차 캐시는 영속성 컨텍스트 내부에 있다. 엔티티 매니저로 조회하거나 변경하는 모든 엔티티는 1차 캐시에 저장된다. 트랜잭션을 커밋하거나 플러시를 호출하면 1차 캐시에 있는 엔티티의 변경 내역을 데이터베이스에 동기화 한다.
JPA를 스프링 프레임워크 같은 컨테이너 위에서 실행하면 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션을 종료할 때 영속성 컨텍스트도 종료한다.
1차 캐시는 끄고 켤 수 있는 옵션이 아니고 영속성 컨텍스트 자체가 사실상 1차 캐시다.

2차 캐시는 애플리케이션 범위의 캐시다. 따라서 애플리케이션을 종료할 때까지 캐시가 유지되며 분산 캐시나 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수도 있다.
2차 캐시를 사용하면 엔티티 매니저를 통해 데이터를 조회할 때 우선 2차 캐시에서 찾고 없으면 데이터베이스에서 찾는다.
2차 캐시는 동시성을 극대화하려고 캐시한 객체를 직접 반환하지 않고 복사본을 만들어서 반환한다.
만약 캐시한 객체를 그대로 반환하면 여러 곳에서 같은 객체를 동시에 수정하는 문제가 발생할 수 있다. 이 문제를 해결하려면 객체에 락을 걸어야 하는데 이렇게 하면 동시성이 떨어질 수 있다.
락에 비하면 객체를 복사하는 비용은 아주 저렴하다. 따라서 2차 캐시는 원본 대신에 복사본을 반환한다.
JPA 구현체 대부분은 캐시 기능을 각자 지원했는데 JPA는 2.0에 와서야 캐시 표준을 정의했다. JPA 캐시 표준은 여러 구현체가 공통으로 사용하는 부분만 표준화 해서 세밀한 설정을 하려면 구현체에 의존적인 기능을 사용해야 한다.
2차 캐시를 사용하려면 엔티티에 javax.persistence.Cacheable 어노테이션을 사용하면 된다.
@Cacheable은 @Cacheable(true), @Cacheable(false)를 설정 가능 기본값은 true이다.
@Cacheable
@Entity
public class Member {
		@Id @GeneratedValue
		private Long id;
		...
}
//persistence.xml에 shard-cache-mode를 설정해서 애플리케이션 전체에(정확히는 영속성 유닛 단위) 
//캐시를 어떻게 적용할지 옵션을 설정해야 한다.
<persistence-unit name="test">
		<shared-cache-mode>ENABLE_SELECTIVE</shared-cahce-mode>
</persistence-unit>
//캐시 모드 스프링 프레임워크 XML 설정
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.
		LocalContainerEntityManagerFactoryBean">
		<property name="sharedCacheMode" value="ENABLE_SELECTIVE" />
		...
캐시 모드는 javax.persistence.SharedCachedMode에 정의되어 있고 보통 ENABLE_SELECTIVE를 사용한다.

캐시를 무시하고 데이터베이스를 직접 조회하거나 캐시를 갱신하려면 캐시 조회 모드와 캐시 보관 모드를 사용하면 된다.
em.setProperty("javax.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS);
캐시 조회 모드나 보관 모드에 따라 프로퍼티와 옵션이 다르다.
//캐시 조회 모드
public eunm CacheRetrieveMode {
	USE,	// (기본값) 캐시에서 조회
    BYPASS	// 캐시를 무시하고 DB에 직접 접근
}    
//캐시 보관 모드
public enum CacheStoreMode {
	USE,	//(기본값) 조회한 데이터 캐시에 저장. 조회한 데이터가 이미 캐시에 있어도 최신화 x, 
    		//트랜잭션을 커밋하면 등록 수정한 엔티티도 캐시에 저장
    BYPASS,	// 캐시에 저장하지 않는다
    REFRESH	// USE 전략에 추가로 DB에서 조회한 엔티티를 최신 상태로 다시 캐시한다.
}    
캐시 모드는 EntityManager.setProperty()로 매니저 단위로 설정하거나 더 세밀하게 EntityManager.find(), EntityManager.refresh()에 설정할 수 있다.
Query.setHint()에도 사용할 수 있다.
//엔티티 매니저 범위
em.setProperty("javax.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS);
em.setProperty("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS);
// find()
Map<String, Object> param = new HashMap<String, Object>();
param.put("javax.persistence.cache.retriveMode", CacheRetrieveMode.BYPASS);
param.put("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS);
em.find(TestEntity.class, id, param);
// JPQL
em.createQuery("select e from TestEntity e where e.id = :id", TestEntity.class)
		.setParameter("id", id)
		.setHint("javax.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS)
		.setHint("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS)
		.getSingleResult();
JPA는 캐시를 관리하기 위한 javax.persistence.Cache 인터페이스를 제공한다. 이것은 EntityManagerFactory에서 구할 수 있다.
// Cache 관리 객체 조회
Cache cache = emf.getCache(); //EntityManageFactory
boolean contains = 
			cache.contains(TestEntity.class, testEntity.getId());
System.out.println("contains = ", + contains);
아래는 Cache 인터페이스의 설명이다.
public interface Cache {
		//해당 엔티티가 캐시에 있는지 여부 확인
		public boolean contains(Class cls, Object primaryKey);
		//해당 엔티티중 특정 식별자를 가진 엔티티를 캐시에서 제거
		public void evict(Class cls, Object primaryKey);
		//해당 엔티티 전체를 캐시에서 제거
		public void evict(Class cls);
		//모든 캐시 데이터 제거
		public void evictAll();
		//JPA Cache 구현체 조회
		public <T> T unwrap(Class<T> cls);
}
하이버네이트와 EHCACHE를 사용해서 실제 2차 캐시를 적용해보자.
참고 : JPA표준에는 엔티티 캐시만 정의되어 있다.
하이버네이트에서 EHCAHE를 사용하려면 hibernate-ehcache 라이브러리를 pom.xml에 추가해야 한다.
해당 라이브러리를 추가하면 net.sf.ehcache-core 라이브러리도 추가된다.
EACACHE는 ehcache.xml을 설정 파일로 사용해서 캐시를 얼마만큼 보관할지, 얼마 동안 보관할지와 같은 캐시 정책 설정 파일이다. 이 파일은 src/main/resources에 두자.
<ehcahce>
		<defaultCache 
				maxElementsInMemory="10000"
				eternal="false"
				timeToldleSeconds="1200"
				timeToLiveSeconds="1200"
				diskExpiryThreadIntervalSeconds="1200"
				memoryStoreEvictionPolicy="LRU"
		/>
</ehcahce>
하이버네이트에서 캐시 사용정보를 설정해야 한다. persistence.xml에 캐시 정보를 추가한다.
<persistence-unit name="test">
		<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
		<properties>
				<property name="hibernate.cache.use_second_level_cache" value"true"/>
				<property name="hibernate.cache.use_query_cache" value"true"/>
				<property name="hibernate.cache.region.factory_class"
										value="org.hibernate.cache.ehcache.EhCacheRegionFactory" />
				<property name="hibernate.generate_statistics" value"true" />
		</properties>
		...
</persistence-unit>
@Cacheable // 1
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // 2
@Entity
public class ParentMember {
		@Id @GeneratedValue
		private Long id;
		
		private String name;
		@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // 3
		@OneToMany(mappedBy = "parentMember", cascade = CascadeType.ALL)
		private List<ChildMember> childMembers = new ArrayList<ChildMember>();
		...
}
org.hibernate.annotations.Cache: 하이버네이트 전용 어노테이션으로 더 세밀한 설정 가능.

캐시 동시성 전략을 설정할 수 있는 usage가 중요하다. CacheConcurrencyStrategy 속성을 보자.

캐시 종류에 따른 동시성 전략 지원 여부는 하이버네이트 공식문서가 제공하는 표를 참고하자.
캐시를 적용한 코드는 캐시 영역(Cache Region)에 저장된다.
엔티티 캐시 영역은 기본값으로 [패키지명 + 클래스명]을 사용한다.
ex) jpabook.jpashop.domain.cache.ParentMember
컬렉션 캐시 영역은 캐시 영역 이름에 캐시한 컬렉션의 필드 명이 추가된다.
ex) jpabook.jpashop.domain.cache.ParentMember.childMembers
@Cache(region = "customRegion", ..._) 처럼 속성을 사용해 캐시 영역을 집정 지정할 수도 있다.
캐시 영역을 위한 접두사를 설정하려면 persistence.xml 설정에 hibernate.cache.region_prefix 사용하면 된다. 예를 들어 core로 설정하면 core.jpabook.jpashop ... 으로 설정하면 된다.
쿼리 캐시는 쿼리와 파라미터 정보를 키로 사용해서 쿼리 결과를 캐시하는 방법이다.
쿼리 캐시를 적용하려면 영속성 유닛을 설정에 hibernate.cache.use_query_cache 옵션을 꼭 true로 설정해야 한다.
또한 쿼리 캐시를 적용하려는 쿼리마다 org.hibernate.cacheable을 true로 설정하는 힌트를 주면 된다.
// 쿼리 캐시 적용
em.createQuery("select i from Item i", Item.class)
  .setHint("org.hibernate.cacheable", true)
  .getResultList();
// NamedQuery에 쿼리 캐시 적용
@Entity
@NamedQuery(
      hints = @QueryHint(name="org.hibernate.cacheable", value="true"), 
      name="Member.findByUsername", query="select m.address from Member m where m.name=:username"
)
public class Member {...}
hibernate.cache.user_query_cache 옵션을 통해 쿼리 캐시를 활성화하면 두 캐시 영역이 추가된다.
쿼리 캐시는 캐시한 데이터 집합을 최신 데이터로 유지하려고 쿼리 캐시를 실행하는 시간과 쿼리 캐시가 사용하는 테이블들이 가장 최근에 변경된 시간을 비교한다. 쿼리 캐시를 적용하고 난 후에 쿼리 캐시가 사용하는 테이블에 조금이라도 변경이 있으면 데이터베이스에서 데이터를 읽어와서 쿼리 결과를 다시 캐시한다. 이제부터 엔티티를 변경하면 org.hibernate.cache.spi.UpdateTimestampsCache 캐시 영역에 해당 엔티티가 매핑한 테이블 이름으로 타임스탬프를 갱신한다.
쿼리 캐시를 잘 활용하면 극적인 성능 향상이 있지만 빈번하게 변경이 있는 테이블에 사용하면 오히려 성능이 더 저하된다. 따라서 수정이 거의 일어나지 않는 테이블에 사용해야 효과를 볼 수 있다.
참고 : org.hibernate.cache.spi.UpdateTimestampsCache 쿼리 캐시 영역은 만료되지 않도록 설정해야 한다. 해당 영역이 만료되면 모든 쿼리 캐시가 무효화된다. EHCACHE의 eternal=”true” 옵션을 사용하면 캐시에서 삭제되지 않는다.
엔티티 캐시를 사용해서 엔티티를 캐시하면 엔티티 정보를 모두 캐시하지만 쿼리 캐시와 컬렉션 캐시는 결과 집합의 식별자 값만 캐시한다. 따라서 쿼리 캐시와 컬렉션 캐시를 조회(캐시 히트)하면 그 안에는 사실 식별자 값만 들어 있다. 그리고 이 식별자 값을 하나씩 엔티티 캐시에서 조회해서 실제 엔티티를 찾는다.
문제는 쿼리 캐시나 컬렉션 캐시만 사용하고 대상 엔티티에 엔티티 캐시를 적용하지 않으면 성능상 심각한 문제가 발생할 수 있다.
쿼리 캐시나 컬렉션 캐시만 사용하고 엔티티 캐시를 사용하지 않으면 최악의 상황에 결과 집합 수만큼 SQL이 실행된다. 따라서 쿼리 캐시나 컬렉션 캐시를 사용하면 결과 대상 엔티티에는 꼭 엔티티 캐시를 적용해야 한다.
참조 : [자바 ORM 표준 JPA 프로그래밍]