Reference. 자바 ORM 표준 JPA 프로그래밍
책 목차 및 이전 글
더보기
들어가기 전 JPA 특징, Q&A
1. JPA 소개
3. 영속성 관리
4. 엔티티 매핑
4.1 - 4.3 @Entity, @Table, 다양한 매핑
4.4 - 4.5 데이터베이스 스키마 자동 생성, DDL 생성 기능
5. 연관관계 매핑 기초
6. 다양한 연관관계 매핑
7. 고급매핑
8. 프록시와 연관관계 관리
9. 값 타입
9.3~5 값 타입과 불변 객체, 값 비교, 값 타입 컬렉션
10. 객체지향 쿼리 언어
11. 웹 애플리케이션 제작
11.1 프로젝트 환경설정
11.2 도메인 모델과 테이블 설계
11.3 애플리케이션 구현
12. 스프링 데이터 JPA
12.1~3 스프링 데이터 JPA 소개, 공통 인터페이스 기능
12.5~10 명세, 사용자 정의 리포지토리, Web 확장...
13. 웹 애플리케이션과 영속성 관리
14. 컬렉션과 부가기능
15. 고급 주제와 성능 최적화
16. 트랜잭션과 락, 2차 캐시
16.2 2차 캐시
16.2.1 1차 캐시와 2차 캐시
- 네트워크를 통해 DB에 접근하는 시간 비용은 애플리케이션 내부 메모리에 접근하는 시간 비용보다 수만에서 수십만 배 이상 비쌈
- 조회한 데이터를 메모리에 캐시해서 DB 접근 횟수를 줄이면 성능이 획기적으로 개선 가능
- 영속성 컨텍스트의 1차 캐시는 트랜잭션을 시작하고 종료할 떄까지만 유효
- OSIV를 사용해도 클라이언트의 요청이 들어올 떄부터 끝날 때까지만 1차 캐시가 유효
- 따라서 애플리케이션 전체로 보면 DB 접근 횟수를 획기적으로 줄이지 못함
- 대부분의 JPA 구현체들은 애플리케이션 범위의 캐시를 지원 (이것이 공유 캐시 또는 2차 캐시)
- 2차 캐시를 활용하면 애플리케이션 조회 성능을 향상, 아래는 그 예시
1차 캐시
- 1차 캐시는 영속성 컨텍스트 내부에 존재하며 엔티티 매니저로 조회, 변경하는 모든 엔티티는 1차 캐시에 저장
- 트랜잭션을 커밋하거나 플러시를 호출하면 1차 캐시에 있는 엔티티의 변경 내역을 DB에 동기화
- OSIV를 사용하면 요청의 시작부터 끝까지 같은 영속성 컨텍스트를 유지
- 1차 캐시는 끄고 키는 옵션이 아니며, 영속성 컨텍스트 자체가 사실상 1차 캐시를 의미
2차 캐시
- 애플리케이션에서 공유하는 캐시를 JPA는 공유 캐시라 하는데 일반적으로 2차 캐시라 부름
- 애플리케이션을 종료할 때까지 캐시가 유지
- 분산 캐시나 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수도 있음
- 엔티티 매니저를 통해 데이터를 조회할 때 우선 2차 캐시에서 조회, 없으면 DB 조회
- 2차 캐시를 적절히 활용하면 데이터베이스 조회 횟수를 획기적으로 감소
- 2차 캐시는 동시성을 극대화하기 위해 객체를 직접 반환하지 않고 복사본을 만들어서 반환
- 2차 캐시의 특징들
- 영속성 유닛 범위의 캐시
- 조회한 객체를 그대로 반환하는 것이 아니라 복사본을 만들어서 반환
- DB 기본 키를 기준으로 캐시하지만 영속성 컨텍스트가 다르면 객체 동일성을 보장하지 않음
16.2.2 JPA 2차 캐시 기능
- JPA 2.0에선 캐시 표준을 정의, 여러 구현체가 공통으로 사용하는 부분만 표준화
- 세밀한 설정을 하려면 구현체에 의존적인 기능을 사용
캐시 모드 설정
- 2차 캐시를 사용하려면 엔티티에
javax.persistence.Cacheable
어노테이션을 사용
- @Cacheable은 @Cacheable(true), @Cacheable(false)를 설정 가능 기본값은 true
- 캐시 모드 설정 예시
@Cacheable @Entity public class Member { @Id @GeneratedValue private Long id; ... }
- persistence.xml에 share-cache-mode를 설정해 애플리케이션 전체에 캐시 적용 옵션을 설정
<persistence-unit name="test"> <shared-cache-mode>ENABLE_SELECTIVE</shared-cahce-mode> </persistence-unit>
- 스프링 프레임워크를 사용할 때 설정하는 방법
<bean id="entityManagerFactory" class="org.springframework.orm.jpa. LocalContainerEntityManagerFactoryBean"> <property name="sharedCacheMode" value="ENABLE_SELECTIVE" /> ... </bean>
SharedCacheMode 캐시 모드 설정
캐시 모드 | 설명 |
---|---|
ALL | 모든 엔티티를 캐시 |
NONE | 캐시를 사용하지 않음 |
ENABLE_SELECTIVE | Cacheable(true)로 설정된 엔티티만 캐시 적용 |
DISABLE_SELECTIVE | 모든 엔티티를 캐시하는데 Cacheable(false)로 명시된 엔티티는 캐시 안함 |
UNSPECIFIED | JPA 구현체가 정의한 설정을 따름 |
캐시 조회, 저장 방식 설정
- 캐시를 무시하고 DB를 직접 조회하거나 캐시를 갱신하려면 캐시 조회 모드와 보관 모드를 사용
em.setProperty("javax.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS);
- 캐시 조회 모드나 보관 모드에 따라 사용할 프로퍼티 옵션이 다름
- 프로퍼티 이름
javax.persistence.cache.retrieveMode
: 캐시 조회 모드 프로퍼티 이름
javax.persistence.cache.storeMode
: 캐시 보관 모드 프로퍼티 이름
- 옵션
javax.persistence.CacheRetrieveMode
: 캐시 조회 모드 설정 옵션
javax.persistence.CacheStoreMode
: 캐시 보관 모드 설정 옵션
- 프로퍼티 이름
CacheRetrieveMode
옵션USE
: 캐시에서 조회, 기본값
BYPASS
: 캐시를 무시하고 데이터베이스에 직접 접근
CacheStoreMode
옵션USE
: 조회한 데이터를 캐시에 저장, 조회한 데이터가 이미 캐시에 있으면 캐시 데이터를 최신 상태로 갱신하지 않음, 트랜잭션을 커밋하면 등록 수정한 엔티티도 캐시에 저장. 기본값
BYPASS
: 캐시에 저장하지 않음
REFRESH
: USE 전략에 추가로 DB에 조회한 엔티티를 최신 상태로 다시 캐시
- 엔티티 매니저 범위 예시
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 캐시 관리 API
- JPA는 캐시를 관리하기 위한
javax.persistence.Cache
인터페이스를 제공 (EntityManageFactory에서 객체 반환
)
- 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); }
16.2.3 하이버네이트와 EHCACHE 적용
- 하이버네이트가 지원하는 캐시는 크게 3가지로 분류
엔티티 캐시
: 엔티티 단위로 캐시, 식별자로 엔티티를 조회하거나 컬렉션이 아닌 연관된 엔티티를 로딩할 때 사용
컬렉션 캐시
: 엔티티와 연관된 컬렉션을 캐시, 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시 (하이버네이트 기능)
쿼리 캐시
: 쿼리와 파라미터 정보를 키로 사용해서 캐시, 결과가 엔티티면 식별자 값만 캐시
- 참고로 JPA 표준에는 엔티티 캐시만 정의
환경설정
- 하이버네이트에서 EHCACHE를 사용하려면
hibernate-ehcache
라이브러리 추가 필요
- EHCACHE는 ehcache.xml을 설정 파일로 사용, 설정 파일은 캐시를 얼마만큼 보관할지와 얼마 동안 보관할지와 같은 캐시 정책을 정의 (자세한 내용은 EHCACHE 공식 문서 참고)
- ehcache.xml 추가 예시
<!-- 파일경로 src/main/resources --> <ehcahce> <defaultCache maxElementsInMemory="10000" eternal="false" timeToldleSeconds="1200" timeToLiveSeconds="1200" diskExpiryThreadIntervalSeconds="1200" memoryStoreEvictionPolicy="LRU" /> </ehcahce>
- 하이버네이트에 캐시 사용정보 설정 예시
<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>
- 설정한 속성 정보에 대한 설명
use_second_level_cache
: 2차 캐시를 활성화, 엔티티 캐시와 컬랙션 캐시를 사용
use_query_cache
: 쿼리 캐시를 활성화
factory_class
: 2차 캐시를 처리할 클래스를 지정 여기서는 EHCACHE를 사용하므로EhCacheRegionFactory
로 지정
generate_statistics
: 하이버네이트가 여러 통계정보를 출력해주며 캐시 적용 여부 확인 가능 (성능에 영향을 주므로 개발 환경에서만 적용하는 것을 추천)
엔티티 캐시와 컬렉션 캐시
- 캐시 적용 코드
@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>(); ... }
@Cacheable
: 엔티티를 캐시하려면 1번 처럼 어노테이션을 적용
@Cache
: 하이버네이트 전용 어노테이션이며, 2번은 세밀한 설정을 할 때 사용하는 예시이며 3번은 컬렉션 캐시를 적용할 떄의 예시
@Cache
- 하이버네이트 전용 어노테이션이며, 이것을 사용하면 세밀한 캐시 설정이 가능
@Cache 속성
속성 | 설명 |
---|---|
usage | CacheConcurrencyStrategy를 사용해서 캐시 동시성 전략을 설정 |
region | 캐시 지역 설정 |
include | 연관 객체를 캐시에 포함할지 선택, all, non-lazy옵션 선택 가능하며 기본값은 all |
- 중요한 것은 캐시 동시성 전략을 설정하는 usage 속성
CacheConcurrencyStrategy 속성
속성 | 설명 |
---|---|
NONE | 캐시를 설정하지 않음 |
READ_ONLY | -읽기 전용으로 설정 -등록, 삭제는 가능하지만 수정은 불가능 -읽기 전용은 수정되지 않으므로 2차 캐시를 조회할 때 객체를 복사하지 않고 원본 객체를 반환 |
NONSTRICT_READ_WRITE | -엄격하지 않은 읽고 쓰기 전략 -동시에 같은 엔티티를 수정하면 데이터 일관성이 깨짐 -EHCACHE는 데이터를 수정하면 캐시 데이터를 무효화 처리 |
READ_WRITE | -읽기 쓰기가 가능하고 READ COMMITTED 정도의 격리 수준을 보장 -EHCACHE는 데이터를 수정하면 캐시 데이터도 같이 수정 |
TRANSACTIONAL | -컨테이너 관리 환경에서 사용 -설정에 따라 REPEATABLE READ 정도의 격리 수준을 보장 |
- 캐시 종류에 따른 동시성 전략 지원 여부는 하이버네이트 공식 문서를 참고
- ConcurrentHashMap은 개발 시에만 사용
캐시 동시성 전략 지원 여부
Cache | read-only | nonstrict-read-write | read-write | transactional |
---|---|---|---|---|
ConcurrentHashMap | yes | yes | yes | |
EHCache | yes | yes | yes | yes |
Infinispan | yes | yes |
캐시 영역
- 캐시를 적용한 코드는 캐시 영역(Cache Region)에 저장
- 엔티티 캐시 영역은 기본값으로 [패키지명 + 클래스명]을 사용
- Ex)
jpabook.jpashop.domain.cache.ParentMember
- Ex)
- 컬렉션 캐시 영역은 캐시 영역 이름에 캐시한 컬렉션의 필드 명이 추가
- Ex)
jpabook.jpashop.domain.cache.ParentMember.childMembers
- Ex)
@Cache(region = "customRegion", ..._)
처럼 속성을 사용해 직접 지정 가능
- 캐시 영역을 위한 접두사를 설정하려면
persistence.xml
설정에hibernate.cache.region_prefix
사용- Ex) core로 설정하면
core.jpabook.jpashop ...
으로 설정
- Ex) core로 설정하면
- 캐시 영역이 정해져 있으므로 세부 설정이 가능
- EHCACHE 세부 설정예시 (ParentMember를 600초 마다 캐시에서 제거)
<ehcahce> <defaultCache maxElementsInMemory="10000" eternal="false" timeToldleSeconds="1200" timeToLiveSeconds="1200" diskExpiryThreadIntervalSeconds="1200" memoryStoreEvictionPolicy="LRU"/> <cache name="jpabook.jpashop.domain.cache.ParentMember" maxElementsInMemory="10000" eternal="false" timeToldleSeconds="600" timeToLiveSeconds="600" overflowToDisk="false"/> </ehcahce>
- EHCACHE 세부 설정예시 (ParentMember를 600초 마다 캐시에서 제거)
쿼리 캐시
- 쿼리 캐시는 쿼리와 파라미터 정보를 키로 사용해서 결과를 캐시하는 방법
- 영속성 유닛을 설정에
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.internal.StandardQueryCache
:- 쿼리 캐시를 지정하는 영역
- 쿼리, 쿼리 결과 집함, 쿼리를 실행한 시점의 타임스탬프를 보관
org.hibernate.cache.spi.UpdateTimestampsCache
:- 쿼리 캐시가 유효한지 확인하기 위해 쿼리 대상 테이블의 가장 최근 변경 시간을 저장하는 영역
- 테이블 명과 테이블의 최근 변경된 타임스탬프를 보관
- 쿼리 캐시는 캐시한 데이터 집합을 최신 데이터로 유지하려고 캐시를 실행하는 시간과 쿼리 캐시가 사용하는 테이블들이 가장 최근에 변경된 시간을 비교
- 캐시를 적용 후 캐시가 사용하는 테이블에 조금이라도 변경이 있으면 DB에서 데이터를 읽어와서 쿼리 결과를 다시 캐시
- 엔티티를 변경하면
UpdateTimestampsCache
캐시 영역에 엔티티가 매핑한 테이블 이름으로 타임스탬프를 갱신
- 쿼리 캐시 사용 예시
public List<ParentMember> findParentMembers() { return em.createQuery("select p from ParentMember p join p.childMembers c", ParentMember.class) .setHint("org.hibernate.cacheable", true) .getResultList(); }
- 쿼리를 실행하면
StandardQueryCache
영역에 타임스탬프를 조회
- 쿼리가 사용하는 엔티티의 테이블인
PARENTMEMBER
,CHILDMEMBER
를UpdateTimestampsCache
캐시 영역에서 테이블들의 타임스탬프를 확인
StandardQueryCache
영역의 타임스탬프가 더 오래되면 DB에서 데이터를 조회해서 다시 캐시
- 쿼리 캐시를 잘 활용하면 성능 향상이 있지만 빈번하게 변경되는 테이블에선 성능이 더 저하
<주의>
UpdateTimestampsCache
캐시 영역은 만료되지 않도록 설정
- 해당 영역이 만료되면 모든 쿼리 캐시가 무효화
- EHCACHE의
eternal="true"
옵션을 사용하면 캐시에서 삭제되지 않음<cache name="org.hibernate.cache.spi.UpdateTimestampsCache" maxElementsInMemory="10000" eternal="true" />
쿼리 캐시와 컬렉션 캐시의 주의점
- 엔티티 캐시를 사용해서 엔티티를 캐시하면 엔티티 정보가 모두 캐시
- 쿼리 캐시와 컬렉션 캐시는 결과 집합의 식별자 값만 캐시
- 쿼리 캐시와 컬렉션 캐시를 조회하면 식별자 값만 존재
- 식별자 값을 하나씩 엔티티 캐시에서 조회해서 실제 엔티티를 조회
- 쿼리 캐시나 컬렉션 캐시만 사용하고 대상 엔티티에 캐시를 적용하지 않으면 심각한 문제 발생
- 심각한 문제가 발생하는 예시
select m from Member m
쿼리를 실행했는데 쿼리 캐시가 적용된 상태, 결과 집합은 100건
- 결과 집합에는 식별자만 있으므로 한 건씩 엔티티 캐시 영역에서 조회
- Member 엔티티는 엔티티 캐시를 사용하지 않으므로 한 건씩 DB에서 조회
- 100건의 SQL이 실행
- 쿼리 캐시나 컬렉션 캐시만 사용하고 엔티티 캐시를 사용하지 않으면 집합 수만큼 SQL이 실행
- 쿼리 캐시나 컬렉션 캐시를 사용하면 결과 대상 엔티티에는 꼭 엔티티 캐시 적용 필요
'개발서적 > 자바 ORM 표준 JPA' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 16.1 트랜잭션과 락 (0) | 2021.10.25 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 15.4 성능 최적화 (0) | 2021.10.25 |
[자바 ORM 표준 JPA 프로그래밍] 15.3 프록시 심화 주제 (0) | 2021.10.25 |
[자바 ORM 표준 JPA 프로그래밍] 15.2 엔티티 비교 (0) | 2021.10.25 |
[자바 ORM 표준 JPA 프로그래밍] 15.1 예외 처리 (0) | 2021.10.25 |