트랜잭션과 락
- 트랜잭션과 격리 수준
트랜잭션은 ACID라 하는 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 함
문제는 격리성인데 트랜잭션 간에 격리성을 완벽히 보장하려면 트랜잭션을 거의 차례대로 실행해야 함
이렇게 하면 동시성 처리 성능이 매우 나빠지므로 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의함
- ACID
- 원자성
트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 함 - 일관성
모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 함
예) 데이터베이스에서 정한 무결성 제약 조건을 항상 만족 - 격리성
동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리해야 함
예) 동시에 같은 데이터를 수정하지 못하도록 해야 함
격리성은 동시성과 관련된 성능 이슈로 인해 격리 수준을 선택할 수 있음 - 지속성
트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 하며 중간에 시스템에 문제가 발생해도
데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 함
- 원자성
- 트랜잭션 격리 수준
READ UNCOMMITED의 격리 수준이 가장 낮고 SERIALZABLE의 격리 수준이 가장 높음
격리 수준이 낮을수록 동시성은 증가하지만 격리 수준에 따른 다양한 문제가 발생
애플리케이션 대부분은 동시성 처리가 중요하므로 데이터베이스들은 보통 READ COMMITED 격리 수준을 기본으로 사용
일부 중요한 비즈니스 로직에 더 높은 격리 수준이 필요하면 데이터베이스 트랜잭션이 제공하는 잠금 기능을 사용
- READ UNCOMMITED (커밋되지 않은 읽기)
== DIRTY READ를 허용하는 격리 수준
커밋하지 않은 데이터를 읽을 수 있어 트랜잭션 1이 데이터를 수정하고 있는데 커밋하지 않아도
트랜잭션 2가 수정 중인 데이터를 조회할 수 있는 DIRTY READ가 가능함
이 경우 트랜잭션 2가 DIRTY READ한 데이터를 사용하는데 트랜잭션 1이 롤백하면 데이터 정합성에 심각한 문제 발생 - READ COMMITED (커밋된 읽기)
== DIRTY READ는 허용하지 않지만, NON-REPEATABLE READ는 허용하는 격리 수준
커밋한 데이터만 읽을 수 있어 DIRTY READ가 발생하지 않음
그러므로 트랜잭션 1이 회원 A를 조회 중인데 갑자기 트랜잭션 2가 회원 A를 수정하고 커밋하면
트랜잭션 1이 다시 회원 A를 조회했을 때 수정된 데이터가 조회됨
이처럼 반복해서 같은 데이터를 읽을 수 없는 상태를 NON-REPEATABLE READ라고 함 - REPEATABLE READ (반복 가능한 읽기)
== NON-REPEATABLE READ는 허용하지 않지만 PHANTPM READ는 허용하는 격리 수준
한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회됨
하지만 트랜잭션 1이 10살 이하의 회원을 조회했는데 트랜잭션 2가 5살 회원을 추가하고 커밋하면
트랜잭션 1이 다시 10살 이하의 회원을 조회했을 때 회원 하나가 추가된 상태로 조회되어
이처럼 반복 조회 시 결과 집합이 달라지는 PHANTON READ가 발생함 - SERIALZABLE (직렬화 기능)
== PHANTPM READ가 발생하지 않는 격리 수준
가장 엄격한 트랜잭션 격리 수준이지만 동시성 처리 성능이 급격히 떨어질 수 있음
- READ UNCOMMITED (커밋되지 않은 읽기)
- ACID
- 낙관적 락과 비관적 락 기초
JPA의 영속성 컨텍스트를 적절히 활용하면
데이터베이스 트랜잭션이 READ COMMITED 격리 수준이어도 애플리케이션 레벨에서 반복 가능한 읽기가 가능함
물론 엔티티가 아닌 스칼라 값을 직접 조회하면 영속성 컨텍스트의 관리를 받지 못하므로 반복 가능한 읽기가 불가능
JPA는 데이터베이스 트랜잭션 격리 수준을 READ COMMITED 정도로 가정하고
만약 일주 로직에 더 높은 격리 수준이 필요하면 낙관적 락과 비판적 락 중 하나를 사용하면 됨
추가로 데이터베이스 트랜잭션 범위를 넘어서는 문제인 두 번의 갱신 분실 문제가 존재함
이는 사용자 A와 B가 동시에 제목이 같은 공지사항을 수정한다고 했을 때 사용자 A가 먼저 수정완료 버튼을 누른 후
사용자 B가 수정완료 버튼을 눌러 사용자 A의 수정사항이 사라지게 되는 것을 뜻함
두 번의 갱신 분실 문제는 데이터베이스 트랜잭션의 범위를 넘어서므로 트랜잭션만으로는 문제를 해결할 수 없어
3가지 선택 방법인 마지막 커밋만 인정하기, 최초 커밋만 인정하기, 충돌하는 갱신 내용 병합하기 방법 중 하나를 사용
기본은 마지막 커밋만 인정하기가 사용되지만 상황에 따라 최초 커밋만 인정하기가 더 합리적일 수 있음
이를 구현하기 위해서는 JPA가 제공하는 버전 관리 기능을 사용해 최초 커밋만 인정하기를 구현할 수 있음
충돌하는 갱신 내용 병합하기는 최초 커밋만 인정하기를 조금 더 우아하게 처리하는 방법으로
애플리케이션 개발자가 직접 사용자를 위해 병합 방법을 제공해야 함
- 낙관적 락
트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법
데이터베이스가 제공하는 락 기능을 사용하는 것이 아니라 JPA가 제공하는 버전 관리 기능을 사용하므로
즉, 애플리케이션이 제공하는 락을 뜻하며 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없음 - 비관적 락
트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 걸고 보는 방법
데이터베이스가 제공하는 락 기능을 사용하며, 대표적으로 select for update 구문이 존재
- 낙관적 락
- @Version
JPA가 제공하는 낙관적 락을 사용하려면 @Version 어노테이션을 사용해서 버전 관리 기능을 추가해야 함
@Version 적용 가능 타입에는 Long, Integer, Short, Timestamp가 존재
엔티티에 버전 관리용 필드를 하나 추가하고 @Version을 붙이면 되며 엔티티를 수정할 때마다 버전이 하나씩 자동으로 증가함
그리고 엔티티를 수정할 때 조회 시점의 버전과 수정 시점의 버전이 다르면 예외가 발생
예) 트랜잭션 1이 조회한 엔티티를 수정하고 있는데 트랜잭션 2에서 같은 엔티티를 수정하고 커밋해서 버전이 충돌하면
트랜잭션 1이 커밋할 때 버전 정보가 다르므로 예외가 발생
그러므로 버전 정보를 사용하면 최초 커밋만 인정하기가 적용됨
JPA가 버전 정보를 비교하는 방법은 단순함
엔티티를 수정하고 트랜잭션을 커밋하면 영속성 컨텍스트를 플러시하면서 UPDATE 쿼리를 실행하며
이때 버전을 사용하는 엔티티면 검색 조건에 엔티티의 버전 정보를 추가하여
데이터베이스 버전과 엔티티 버전이 같으면 데이터를 수정하면서 동시에 버전도 하나 증가시킴
만약 데이터베이스에 버전이 이미 증가해서 수정 중인 엔티티의 버전과 다르면 수정할 대상이 없으므로 JPA가 예외를 발생시킴
즉, 버전은 엔티티의 값을 변경하면 증가하는 것으로
값 타입인 임베디드 타입과 값 타입 컬렉션은 논리적인 개념상 해당 엔티티의 값이므로 수정하면 엔티티의 버전이 증가하게 됨
단 연관관계 필드는 외래 키를 관리하는 연관관계의 주인 필드를 수정할 때만 버전이 증가하게 됨
@Version으로 추가한 버전 관리 필드는 JPA가 직접 관리하므로 개발자가 임의로 수정하면 안 되며
만약 버전 값을 강제로 증가하려면 특별한 락 옵션을 선택하면 됨
// 엔티티에 버전 관리 추가
@Entity
public class Board {
@Id
private String id;
private String title;
// 엔티티에 버전 관리용 필드를 하나 추가하고 @Version을 붙이면 됨
// 이제부터 엔티티를 수정할 때마다 버전이 하나씩 자동으로 증가함
// 그리고 엔티티를 수정할 때 조회 시점의 버전과 수정 시점의 버전이 다르면 예외가 발생
@Version
private Integer 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
// 버전 사용 엔티티 SQL
UPDATE BOARD
SET
TITLE=?,
VERSION=? (버전 + 1 증가)
WHERE
ID=?
AND VERSION=? (버전 비교)
// 데이터베이스에 버전이 이미 증가해서 수정 중인 엔티티의 버전과 다르면
// UPDATE 쿼리의 WHERE 문에서 VERSION 값이 다르므로 수정할 대상이 없게 됨
- JPA 락 사용
JPA가 제공하는 락은 다음 위치에 적용할 수 있음
EntityManager.lock(), EntityManager.find(), EntityManager.refresh(), Query.setLockMode(), @NamedQuery
즉시 락을 걸 수도 있고 필요할 때 락을 걸 수도 있음
JPA가 제공하는 락 옵션은 javax.persistence.LockModeType에 정의되어 있음
// 조회하면서 즉시 락 걸기
Board board = em.find(Board.class, id, LockModeType.OPTIMISTIC);
// 필요할 때 락 걸기
Board board = em.find(Board.class, id);
...
em.lock(board, LockModeType.OPTIMISTIC);
- JPA 낙관적 락
JPA가 제공하는 낙관적 락은 버전을 사용하므로 낙관적 락을 사용하려면 버전이 있어야 함
낙관적 락은 트랜잭션을 커밋하는 시점에 충돌을 알 수 있다는 특징이 존재
참고로 락 옵션 없이 @Version만 있어도 낙관적 락이 적용되지만 락 옵션을 사용하면 락을 더 세밀하게 제어할 수 있음
낙관적 락에서 발생하는 예외에는 3가지가 존재
- javax.persistence.OptimisticLockException (JPA 예외)
- org.hibernate.StaleObejctStateException (하이버네이트 예외)
- org.springframework.orm.ObjectOptimisticLockingFailureException (스프링 예외 추상화)
- JPA 낙관적 락 - (1) NONE
락 옵션을 적용하지 않아도 엔티티에 @Version이 적용된 필드만 있으면 낙관적 락이 적용됨
조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경되지 않아야 하므로 조회 시점부터 수정 시점까지를 보장함
엔티티를 수정할 때 버전을 체크하면서 버전을 증가시키며 데이터베이스의 버전 값이 현재 버전이 아니면 예외가 발생함
이를 통해 두 번의 갱신 분실 문제를 예방할 수 있음 - JPA 낙관적 락 - (2) OPTIMISTIC
@Version만 적용했을 때는 엔티티를 수정해야 버전을 체크하지만 이 옵션을 추가하면 엔티티를 조회만 해도 버전을 체크
그러므로 한 번 조회한 엔티티는 트랜잭션을 종료할 때까지 다른 트랜잭션에서 변경하지 않음을 보장함
트랜잭션을 커밋할 때 버전 정보를 조회해서 현재 엔티티의 버전과 같은지 검증하고 같지 않으면 예외가 발생함
이는 DRITY READ와 NON-REPEATABLE READ를 방지할 수 있음
// OPTIMISTIC 예제
// 트랜잭션 1 조회 title="제목A", version=1
Board board = em.find(Board.class, id, LockModeTyep.OPTIMISTIC);
// 중간에 트랜잭션 2에서 해당 게시물을 수정해서 title="제목C", version=2로 증가
// 트랜잭션 1 커밋 시점에 SELECT 쿼리로 조회해서 버전 정보 검증, 예외 발생
// (데이터베이스 version=2, 엔티티 version=1)
tx.commit();
- JPA 낙관적 락 - (3) OPTIMISTIC_FORCE_INCREMENT
강제로 버전을 증가해서 논리적인 단위의 엔티티 묶음을 버전 관리할 수 있음
예를 들어 게시물과 첨부파일이 일대다, 다대일의 양방향 연관관계이고 첨부파일이 연관관계의 주인일 때
게시물을 수정하는데 단순히 첨부파일만 추가하면 게시물의 버전을 증가하지 않는데 해당 게시물은 논리적으로 변경되었으므로
게시물의 버전도 강제로 증가시키기 위해서는 OPTIMISTIC_FORCE_INCREMENT를 사용
이를 통해 엔티티를 수정하징 않아도 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해서 버전 정보를 강제로 증가시킴
이때 데이터베이스의 버전이 엔티티의 버전과 다르면 예외가 발생하고,
추가로 엔티티를 수정하면 수정 시 버전 UPDATE가 발생하여 총 2번의 버전 증가가 나타날 수 있음
// OPTIMISTIC_FORCE_INCREMENT 예제
// 트랜잭션 1 조회 title="제목A", version=1
Board board = em.find(Board.class, id, LockModeTyep.OPTIMISTIC_FORCE_INCREMENT);
// 트랜잭션 1 커밋 시점에 버전 강제 증가
tx.commit();
- JPA 비관적 락
JPA가 제공하는 비관적 락은 데이터베이스 트랜잭션 락 메커니즘에 의존하는 방법
주로 SQL 쿼리에 select for update 구문을 사용하면서 시작하고 버전 정보는 사용하지 않음
비관적 락은 주로 PESSIMISTIC_WRITE 모드를 사용함
비관적 락은 엔티티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있으며 데이터를 수정하는 즉시 트랜잭션 충돌 감지 가능
비관적 락에서 발생하는 예외에는 2가지가 존재
- javax.persistence.PessimisticLockException (JPA 예외)
- org.springframework.dao.PessimisticLockingFailureException (스프링 예외 추상화)
- JPA 비관적 락 - (1) PESSIMISTIC_WRITE
비관적 락이라 하면 일반적으로 이 옵션을 뜻하며 데이터베이스에 쓰기 락을 걸 때 사용함
데이터베이스에 select for update를 사용해서 락을 걸며 NON-REPEATABLE READ를 방지하고
락이 걸린 로우는 다른 트랜잭션이 수정할 수 없음 - JPA 비관적 락 - (2) PESSIMISTIC_READ
데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용하며 일반적으로 잘 사용하지 않음
데이터베이스 대부분은 방언에 의해 PESSIMISTIC_WRITE로 동작함
MySQL에서는 lock in share mode, PostgreSQL에서는 for share로 동작 - JPA 비관적 락 - (3) PESSIMISTIC_FORCE_INCREMENT
비관적 락 중 유일하게 버전 정보를 사용하며 버전 정보를 강제로 증가시킴
하이버네이트는 nowait를 지원하는 데이터베이스에 대해서 for update nowait 옵션을 적용하고 Postgresql 또한 마찬가지
nowait를 지원하지 않으면 for update가 사용됨 - 비관적 락과 타임아웃
비관적 락을 사용하면 락을 획득할 때까지 트랜잭션이 대기하게 되는데, 무한정 기다릴 수 없으므로 타임아웃 시간을 줄 수 있음
하지만 타임아웃은 데이터베이스 특성에 따라 동작하지 않을 수 있음
// 10초간 대기해서 응답이 없으면 javax.persistence.LockTimeoutException 예외가 발생
Map<String, Object> properties = new HashMap<String, Object>();
// 타임아웃 10초까지 대기 설정
properties.put("javax.persistence.LockTimeoutException", 10000);
Board board = em.find(Board.class, "boardId", LockModeType.PESSIMISTIC_WRITE, properties);
2차 캐시
- 1차 캐시와 2차 캐시
네트워크를 통해 데이터베이스에 접근하는 비용은 애플리케이션 서버에서 내부 메모리에 접근하는 시간 비용보다 훨씬 비쌈
따라서 조회한 데이터를 메모리에 캐시해서 데이터베이스 접근 횟수를 줄이면 애플리케이션 성능을 획기적으로 개선 가능
영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데 이것을 1차 캐시라고 함
하지만 이러한 일반적인 웹 애플리케이션 환경은 트랜잭션을 시작하고 종료할 때까지 1차 캐시가 유효함
OSIV를 사용해도 클라이언트의 요청이 들어올 때부터 끝날 때까지만 1차 캐시가 유용함
따라서 애플리케이션 전체로 보면 데이터베이스 접근 횟수를 획기적으로 줄이지 못함
하이버네이트를 포함한 대부분의 JPA 구현체들은 애플리케이션 범위의 캐시를 지원하는데 이것을 공유 캐시 또는 2차 캐시라 함
이런 2차 캐시를 활용하면 애플리케이션 조회 성능을 향상할 수 있음
- 1차 캐시
1차 캐시는 영속성 컨텍스트 내부에 있어 엔티티 매니저로 조회하거나 변경하는 모든 엔티티는 이에 저장됨
그리고 트랜잭션을 커밋하거나 플러시를 호출하면 1차 캐시에 있는 엔티티의 변경 내역을 데이터베이스 동기화함
즉, 영속성 컨텍스트 자체가 사실상 1차 캐시
JPA를 J2EE나 스프링 프레임워크 같은 컨테이너 위에서 실행하면 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고
트랜잭션을 종료할 때 영속성 컨텍스트도 종료하며 OSIV를 사용하면 요청의 시작부터 끝까지 같은 영속성 컨텍스트를 유지
1차 캐시의 동작 방식과 특징 분석
- 1차 캐시의 동작 방식
- 최초 조회할 때는 1차 캐시에 엔티티가 없으므로
- 데이터베이스에서 엔티티를 조회해서
- 1차 캐시에 보관하고
- 1차 캐시에 보관한 결과를 반환함
- 이후 같은 엔티티를 조회하면 1차 캐시에 같은 엔티티가 있으므로 데이터베이스를 조회하지 않고
1차 캐시의 엔티티를 그대로 반환
- 1차 캐시의 특징
- 1차 캐시는 같은 엔티티가 있으면 해당 엔티티를 그대로 반환하므로 객체 동일성을 보장
- 1차 캐시는 기본적으로 영속성 컨텍스트 범위의 캐시
- 1차 캐시의 동작 방식
- 2차 캐시
애플리케이션에서 공유하는 캐시를 JPA는 공유 캐시라고 하며 2차 캐시라고 부르며 애플리케이션 범위의 캐시를 뜻함
따라서 애플리케이션을 종료할 때까지 캐시가 유지됨
2차 캐시를 적용하면 엔티티 매니저를 통해 데이터를 조회할 때 우선 2차 캐시에서 찾고 없으면 데이터베이스에서 찾음
2차 캐시를 적절히 활용하면 데이터베이스 조회 횟수를 획기적으로 줄일 수 있음
2차 캐시의 동작 방식과 특징 분석
- 2차 캐시의 동작 방식
- 영속성 컨텍스트는 엔티티가 필요하면 2차 캐시를 조회함
- 2차 캐시에 엔티티가 없으면 데이터베이스를 조회해서
- 결과를 2차 캐시에 보관함
- 2차 캐시는 자신이 보관하고 있는 엔티티를 복사해서 반환함
- 2차 캐시에 저장되어 있는 엔티티를 조회하면 복사본을 만들어 반환함
(동시성을 극대화하려고 캐시한 객체를 직접 반환하지 않고 복하본을 만들어서 반환하는 것
만약 캐시한 객체를 그대로 반환하면 여러 곳에서 같은 객체를 동시에 수정하는 문제가 발생할 수 있으며
이를 해결하기 위해 객체에 락을 걸 경우 동시성이 떨어질 수 있음)
- 2차 캐시의 특징
- 2차 캐시는 영속성 유닛 범위의 캐시
- 2차 캐시는 조회한 객체를 그대로 반환하는 것이 아니라 복사본을 만들어서 반환함
- 2차 캐시는 데이터베이스 기본 키를 기준으로 캐시하지만 영속성 컨텍스트가 다르면 객체 동일성을 보장하지 않음
- 2차 캐시의 동작 방식
- JPA 2차 캐시 기능
JPA 구현체 대부분은 캐시 기능을 각자 지원했지만 2.0에 와서 캐시 표준을 정의했음
JPA 캐시 표준은 여러 구현체가 공통 사용하는 부분만 표준화해서 세밀한 설정을 하려면 구현체에 의존적인 기능을 사용해야 함 - JPA 2차 캐시 기능 - (1) 캐시 모드 설정
2차 캐시를 사용하려면 엔티티에 Cacheable 어노테이션을 사용하면 되고 @Cacheable(true), @Cacheable(false)로 설정
다음으로는 persistence.xml에 shared-cache-mode를 설정해서 애플리케이션 전체에 캐시를 어떻게 적용할지 옵션 설정
캐시 모드는 javax.persistence.SharedCacheMode에 정의되어 있고 보통 ENABLE_SELECTIVE를 사용
- ALL : 모든 엔티티를 캐시함
- NONE : 캐시를 사용하지 않음
- ENABLE_SELECTIVE : Cacheable(true)로 설정된 엔티티만 캐시를 적용
- DISABLE_SELECTIVE : 모든 엔티티를 캐시하는데 Cacheable(false)로 명시된 엔티티는 캐시하지 않음
- UNSPECIFIED : JPA 구현체가 정의한 설정을 따름
// 캐시 모드 설정
@Cacheable
@Entity
public class Member {
@Id @GanaratedValue
private Long id;
...
}
// persistence.xml에 캐시 모드 설정
<persistence-unit name="test">
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
</persistence-unit>
// 캐시 모드 스프링 프레임워크 XML 설정
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name-"sharedCacheMode" value="ENABLE_SELECTIVE"/>
...
- JPA 2차 캐시 기능 - (2) 캐시 조회, 저장 방식 설정
캐시를 무시하고 데이터베이스를 직접 조회하거나 캐시를 갱신하려면 캐시 조회 모드와 캐시 보관 모드를 사용
캐시 조회 모드나 보관 모드에 따라 사용할 프로퍼티와 옵션이 다름
또한 캐시 모드는 EntityManager.setProperty()로 엔티티 매니저 단위로 설정하거나
더 세밀하게 EntityManager.find(), EntityManager.refresh()에 설정할 수 있고 Query.setHint()에도 사용할 수 있음
// 캐시 조회 모드 프로퍼티
javax.persistence.cache.retrieveMode : 캐시 조회 모드 프로퍼티 이름
// 캐시 조회 모드 옵션
javax.persistence.CacheRetrieveMode : 캐시 조회 모드 설정 옵션
// 캐시 조회 모드
public enum CacheRetreiveMode {
USE, // 캐시에서 조회함
BYPASS // 캐시를 무시하고 데이터베이스에 직접 접근
}
// 캐시 보관 모드 프로퍼티
javax.persistence.cache.storeMode : 캐시 보관 모드 프로퍼티 이름
// 캐시 보관 모드 옵션
javax.persistence.CacheStoreMode : 캐시 보관 모드 설정 옵션
// 캐시 보관 모드
public enum CacheStoreMode {
USE, // 조회한 데이터를 캐시에 저장하며 조회한 데이터가 이미 캐시에 있으면 최신 상태로 갱신하지 않음, 트랜잭션을 커밋하면 등록 수정한 엔티티도 캐시에 저장
BYPASS, // 캐시에 저장하지 않음
REFRESH // USE 전략에 추가로 데이터베이스에서 조회한 엔티티를 최신 상태로 다시 캐시
}
// 엔티티 매니저 범위 - 엔티티 매니저 단위로 캐시모드 설정
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.retrieveMode", CacheRetrieveMode.BYPASS");
param.put("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS");
em.find(TestEntity.class, id, param);
// JPQL - Query.setHint()에 캐시 모드 사용
em.createQuery("select e from TestEntity TestEntity e where e.id = :id", TestEntity.class)
.setParameter("id", id)
.setHint("javax.persistence.cache.retrieveMode", CacheRetriveMode.BYPASS)
.setHint("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS)
.getSingleResult();
- JPA 2차 캐시 기능 - (3) JPA 캐시 관리 API
JPA는 캐시를 관리하기 위해 javax.persistence.Cache 인터페이스를 제공하며 EntityManagerFactory에서 구할 수 있음
// Cache 관리 객체 조회
Cache cache = emf.getCache();
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 적용
하이버네이트와 EHACHE를 사용해서 2차 캐시를 적용해보자
하이버네이트가 지원하는 캐시는 3가지가 존재
- 엔티티 캐시
엔티티 단위로 캐시하며 식별자로 엔티티를 조회하거나 컬렉션이 아닌 연관된 엔티티를 로딩할 때 사용함 - 컬렉션 캐시
엔티티와 연관된 컬렉션을 캐시하며 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시함 - 쿼리 캐시
쿼리와 파라미터 정보를 키로 사용해서 캐시하며 결과가 엔티티면 식별자 값만 캐시함
- 엔티티 캐시
- 하이버네이트와 EHCACHE 적용 - (1) 환경설정
하이버네이트에서 EHCACHE를 사용하려면 hibernate-ehacache 라이브러리를 pom.xml에 추가해야 함
hibernate-eacache를 추가하면 net.sf.ehcache-core 라이브러리도 추가됨
EHCACHE는 ehcache.xml을 설정 파일로 사용하며 이는 캐시를 얼마만큼 보관할지, 얼마 동안 보관할지와 같은 캐시 정책 정의
마지막으로 하이버네이트에 캐시 사용정보를 설정하기 위해 persistence.xml에 캐시 정보 추가
// pom.xml에 hibernate-ehcache 추가
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>4.3.10.Final</version>
</denpendency>
// src/main/resources/ehcache.xml 추가
<ehcache>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="1200"
timeToLiveSeconds="1200"
diskExpiryThreadIntervalSeconds="1200"
memoryStoreEvictionPolicy="LRU"
/>
</ehcache>
// persistence.xml에 캐시 정보 추가
<persistence-unit name="test">
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
<properties>
// 2차 캐시를 활성화하며 엔티티 캐시와 컬렉션 캐시를 사용할 수 있음
<property name="hibernate.cache.use_second_level_cache" value="true"/>
// 쿼리 캐시를 활성화
<property name="hibernate.cache.use_query_cache" value="true"/>
// 2차 캐시를 처리할 클래스를 지정
<property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
// 하이버네이트가 여러 통계정보를 출력해주며 캐시 적용 여부를 확인할 수 있음
<property name="hibernate.generate_statistics" value="true"/>
</properties>
...
</persistence-unit>
- 하이버네이트와 EHCACHE 적용 - (2) 엔티티 캐시와 컬렉션 캐시
ParentMember에 엔티티 캐시를 적용하고 ParentMember.childMembers에는 컬렉션 캐시 적용하기
엔티티를 캐시하려면 @Cacheable 어노테이션을 적용하며
@Cache는 하이버네이트 전용 어노테이션이며 캐시와 관련된 더 세밀한 설정을 할 때 사용하고 컬렉션 캐시를 적용할 때도 사용
// 캐시 적용 코드
// 엔티티 캐시
@Cacheable
// 하이버네이트 전용으로 캐시와 관련된 세밀한 설정 가능
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Entity
public class ParentMember {
@Id @GeneratedValue
private Long id;
private String name;
// 컬렉션 캐시 적용
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@OneToMany(mappedBy = "parentMember", cascade = CascadeType.ALL)
private List<ChildeMember> childMembers = new ArraryList<ChildMember>();
...
}
- 하이버네이트와 EHCACHE 적용 - (3) @Cache
하이버네이트 전용인 @Cache 어노테이션을 사용하면 세밀한 캐시 설정이 가능함
- usage
CacheConcurrencyStrategy를 사용해서 캐시 동시성 전략을 설정함
- CacheConcurrencyStrategy.NONE
캐시를 설정하지 않음 - CacheConcurrencyStrategy.READ_ONLY
읽기 전용으로 설정하며 등록, 삭제는 가능하지만 수정은 불가능함
읽기 전용인 불변 객체는 수정되지 않으므로 하이버네이트는 2차 캐시를 조회할 때 복사가 아닌 원본 객체를 반환 - CacheConcurrencyStrategy.NONSTRICT_READ_WRITE
엄격하지 않은 읽고 쓰기 전략으로 동시에 같은 엔티티를 수정하면 데이터 일관성이 깨질 수 있음
EHCACHE는 데이터를 수정하면 캐시 데이터를 무효화 함 - CacheConcurrencyStrategy.READ_WRITE
읽기 쓰기가 가능하고 READ COMMITED 정도의 격리 수준을 보장함
EHCACHE는 데이터를 수정하면 캐시 데이터도 같이 수정함 - CacheConcurrencyStrategy.TRANSACTIONAL
컨테이너 관리 환경에서 사용할 수 있으며 설정에 따라 REPEATABLE READ 정도의 격리 수준을 보장
- CacheConcurrencyStrategy.NONE
- region
캐시 지역 설정 - include
연관 객체를 캐시에 포함할지 선택하며, all, non-lazy 옵션을 선택할 수 있음
- usage
- 하이버네이트와 EHCACHE 적용 - (4) 캐시 영역
캐시를 적용한 코드는 캐시 영역에 저장됨
엔티티 캐시 영역은 기본값으로 [패키지명 + 클래스 명]을 사용
예) jpabook.jpashop.domain.test.cache.ParentMember
컬렉션 캐시 영역은 엔티티 캐시 영역 이름에 캐시한 컬렉션의 필드 명이 추가되며
필요하다면 @Cache(region = "custionRegion", ...) 처럼 region 속성을 사용해서 캐시 영역을 직접 지정할 수 있음
예) jpabook.jpashop.domain.test.cache.ParentMember.childMembers
캐시 영역을 위한 접두사를 설정하려면 persistence.xml 설정에 hibernate.cache.region_prefix를 사용
예) core.jpabook.jpashop...
또한 캐시 영역이 정해져 있으므로 영역별로 세부 설정을 할 수 있음
// EHCACHE 세부 설정 (ehcache.xml)
// ParentMember를 600초마다 캐시에서 제거하고 싶을 때
<ehcache>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="1200"
timeToLiveSeconds="1200"
diskExpiryThreadIntervalSeconds="1200"
memoryStoreEvictionPolicy="LRU"/>
<cache
name="jpabook.jpashop.domain.test.cache.ParentMember"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="600"
overflowToDisk="false" />
</ehcache>
- 하이버네이트와 EHCACHE 적용 - (5) 쿼리 캐시
쿼리 캐시는 쿼리와 파라미터 정보를 키로 사용해서 쿼리 결과를 캐시하는 방법
쿼리 캐시를 적용하려면 영속성 유닛을 설정해 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 Meber {
...
}
- 하이버네이트와 EHCACHE 적용 - (6) 쿼리 캐시 영역
hibernate.cache.use_query_cache 옵션을 true로 설정해서 쿼리 캐시를 활성화하면 두 캐시 영역이 추가됨
쿼리 캐시는 캐시한 데이터 집합을 최신 데이터로 유지하려고 쿼리 캐시를 실행하는 시간과
쿼리 캐시가 사용한 테이블들이 가장 최근에 변경된 시간을 비교함
쿼리 캐시를 적용하고 난 후에 쿼리 캐시가 사용하는 테이블에 조금이라고 변경이 있으면
데이터베이스에서 데이터를 읽어와서 쿼리 결과를 다시 캐시하게 됨
쿼리 캐시를 잘 활용하면 극적인 성능 향상이 있지만 빈번하게 변경이 있는 테이블에 사용하면 오히려 성능이 저하되므로
수정이 거의 일어나지 않는 테이블에 사용해야 효과를 볼 수 있음
- org.hibernate.cache.internal.StandardQueryCache
쿼리 캐시를 저장하는 영역으로 쿼리, 쿼리 결과 집합, 쿼리를 실행한 시점의 타임스탬프를 보관 - org.hibernate.cache.spi.UpdateTimestampsCache
쿼리 캐시가 유효한지 확인하기 위해 쿼리 대상 테이블의 가장 최근 변경 시간을 저장하는 영역으로
테이블 명과 해당 테이블의 최근 변경된 타임스탬프를 보관
- org.hibernate.cache.internal.StandardQueryCache
// 쿼리 캐시 사용
public List<ParentMember> findParentMembers() {
/* 쿼리에서 ParentMember와 ChildMember 엔티티를 사용
쿼리를 실행하면 우선 StandardQueryCache 캐시 영역에서 타임스탬프를 조회하고
쿼리가 사용하는 엔티티의 테이블이 PARENTMEMBER, CHILDMEMBER를 UpdateTimestampCache 캐시 영역에서 조회해서
테이블들의 타임스탬프를 확인하며 만약 StandardQueryCache 캐시 영역의 타임스탬프가 더 오래되었으면
캐시가 유효하지 않은 것으로 보고 데이터베이스에서 데이터를 조회해서 다시 캐시함 */
return em.createQuery("select p from ParentMember p join p.childMembers c", ParentMember.class)
.setHint("org.hibernate.cacheable", true)
.getResultList();
}
- 하이버네이트와 EHCACHE 적용 - (7) 쿼리 캐시와 컬렉션 캐시의 주의점
엔티티 캐시를 사용해서 엔티티를 캐시하면 엔티티 정보를 모두 캐시하지만
쿼리 캐시와 컬렉션 캐시는 결과 집합의 식별자 값만 캐시하므로
쿼리 캐시와 컬렉션 캐시를 조회하면 그 안에 사실 식별자 값만 들어 있음
그리고 이 식별자 값을 하나씩 엔티티 캐시에서 조회해서 실제 엔티티를 찾음
문제는 쿼리 캐시나 컬렉션 캐시만 사용하고 대상 엔티티에 엔티티 캐시를 적용하지 않으면 성능상 심각한 문제가 발생
예) select m from Member m 쿼리를 실행했는데 쿼리 캐시가 적용되어 있어 결과 집합은 100건일 때
결과 집합에는 식별자만 있으므로 한 건씩 엔티티 캐시 영역에서 조회하지만
Member 엔티티는 엔티티 캐시를 사용하지 않으므로 한 건씩 데이터베이스에서 조회하여 100건의 SQL을 실행하게 됨
이처럼 쿼리 캐시나 컬렉션 캐시만 사용하고 엔티티 캐시를 사용하지 않으면 최악의 상황에 따라
결과 집합 수만큼 SQL이 실행되므로 쿼리 캐시나 컬렉션 캐시를 사용하면 결과 대상 엔티티는 꼭 엔티티 캐시를 적용해야 함
'Java-Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
자바 ORM 표준 JPA 프로그래밍 - 목차 (0) | 2023.07.03 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 고급 주제와 성능 최적화 ② (0) | 2022.06.07 |
[자바 ORM 표준 JPA 프로그래밍] 고급 주제와 성능 최적화 ① (0) | 2022.06.04 |
[자바 ORM 표준 JPA 프로그래밍] 컬렉션과 부가 기능 (0) | 2022.05.27 |
[자바 ORM 표준 JPA 프로그래밍] 웹 애플리케이션과 영속성 관리 (0) | 2022.05.26 |