목표: 트랜잭션의 기초와 JPA가 제공하는 낙관적 락과 비관적 락
트랜잭션: ACID라 하는 원자성, 일관성, 격리성, 지속성을 보장해야함
트랜잭션에서 격리성을 완벽히 보장하기 위해서는 트랜잭션을 거의 차례대로 진행해야함
→ 단점: 동시성 처리 성능이 매우 나빠짐
→ 따라서 ANSI 표준에서는 트랜잭션의 격리 수준을 4단계로 나누어 정의함.
READ UNCOMMITED의 격리수준이 가장 낮고, SERIALIZABLE의 격리 수준이 가장 높음.
격리 수준이 낮을 수록 동시성을 증가하지만 다양한 문제 발생 가능.
| 격리수준 | DIRTY READ | NON-REPEATABLE READ | PHANTOM READ |
|---|---|---|---|
| READ UNCOMMITED | O | O | O |
| READ COMMITED | O | O | |
| REPEATABLE READ | O | ||
| SERIALIZABLE |
애플리케이션은 대부분 동시성 처리가 중요하기에 데이터베이스들은 보통 READ COMMITED 격리수준을 기본으로 사용함. 더 높은 수준의 격리 수준이 일부 비즈니스 로직에 필요하면, 데이터베이스 트랜잭션이 제공하는 잠금 기능을 제공을 사용.
JPA는 READ COMMITED로 트랜잭션 격리 수준을 가정. 더 높은 격리 수준일 필요할 때에는 낙관적 락 또는 비관적 락 중 하나를 사용
select for update 구문 존재마지막 커밋만 인정
최초 커밋만 인정
충돌하는 갱신 내용 병합
⇒ 기본적으로는 마지막 커밋만 인정하는 것이 적용됨. 다만, 이는 상황마다 3가지 중 적합한 것이 변화하며 JPA가 제공하는 버전 관리 기능을 사용하면 최초 커밋만 인정하는 것 역시 구현 가능함. 충돌 내용을 병합하기 위해서는 애플리케이션 개발자가 직접 병합 방법을 제공해야함.
JPA가 제공하는 낙관적 락 사용을 위해서 @Version 어노테이션을 사용해 버전 관리 기능을 추가해야함
@Version 어노테이션 적용 가능 타입: Long, Integer, Short, Timestamp
@Entity
public class Board {
@Id
private String id;
private String title;
@Version
private Integer version;
}
@Version 어노테이션 추가 이후에는 엔티티 수정마다 버전이 하나씩 자동으로 추가됨.
⇒ “버전은 엔티티의 값을 변경하면 증가”
@Version으로 추가한 버전 관리 필드의 경우 JPA가 직접 관리하기에, 개발자가 임의로 수정해서는 안됨 (벌크 연산 제외)버전 값을 강제 증가하려면 특별한 락 옵션을 선택
+) 벌크 연산은 버전을 무시. 벌크연산에서 버전을 증가하려면 버전 필드를 강제 증가시켜야함
JPA에서 제공하는 락은 다음 위치에 적용 가능
조회하면서 즉시 락을 걸 수도 있고, 필요 시에 락을 걸 수도 있음.
JPA에서 제공하는 락 옵션은 LockModeType에 정의되어있음.
@Version)을 사용함 → 낙관적 락은 트랜잭션을 커밋하는 시점에 충돌을 알 수 있음락 옵션 없이 @Version만 있어도 낙관적 락이 적용됨.
@Version이 적용된 필드만 있으면 낙관적 락이 적용됨@Version만 적용했을 때는 엔티티를 수정해야 버전을 체크하지만, 이 옵션을 추가하면 엔티티를 조회만 해도 버전을 체크함. 즉, 한 번 조회한 엔티티는 트랜잭션 종료까지 다른 트랜잭션에서 변경하지 않음을 보장함//트랜잭션1 조회 title="제목A", verison = 1
Board board = em.find(Board.class, id, LockModeType.OPTIMISTIC);
//중간에 트랜잭션2에서 해당 게시물 수정해 title = "제목C", version=2로 증가
//트랜잭션 1 커밋 시점에 버전 정보 검증, 예외 발생
//(데이터베이스 version=2, 엔티티 version=1)
tx.commit();
select for update 구문을 사용하며 시작, 버전 정보는 사용하지 않음일반적인 비관적 락의 옵션. 데이터베이스에 쓰기 락을 걸 때 사용
select for update를 사용해 락을 검데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용. 일반적으로 잘 사용하지 않음.
비관적 락 중 유일하게 버전 정보를 사용함. 버전 정보를 강제로 증가시킴. 하이버네이트는 nowait를 지원하는 데이터베이스에 대해 for update nowait 옵션을 적용함
비관적 락을 사용하는 경우 락을 획득할 때까지 트랜잭션이 대기 → 무한정 기다릴 수 없기에 타임아웃 시간을 줌.
응답이 없는 경우 LockTimeoutException이 발생. (다만, 데이터베이스 특성에 따라 동작하지 않을 수 있음)
Map<String, Object>properties = new HashMap<String,Object>();
//타임아웃 10초까지 대기 설정
properties.put("timeout", 10000);
Board board = em.find(Board.class, "boardId", LockModeType.PESSIMISTIC_WRITE,properties);
📌 1차 캐시 특징
a==b)를 보장📌 2차 캐시 특징
a==b)을 보장하지 않음JPA 2.0에서 캐시(2차 캐시) 표준을 정의함. JPA 캐시 표준은 여러 구현체가 공통으로 사용하는 부분만 표준화했기에, 세밀한 설정을 하려면 구현체에 의존적인 기능 사용 필요.
@Cacheable 어노테이션을 사용.@Cacheable
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
}
shared-cache-mode를 설정해 애플리케이션 전체에 캐시를 어떻게 적용할지 옵션을 정해야함<persistence-unit name="test">
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
</persistence-unit>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="sharedCacheMode" value="ENABLE_SELECTIVE"/>
캐시를 무시하고 데이터베이스를 직접 조회하거나 캐시를 갱신하려면 캐시 조회 모드와 캐시 보관 모드를 사용하면 됨.
em.setProperty("retrieveMode", CacheRetrieveMode.BYPASS);
public enum CacheRetrieveMode {
USE,
BYPASS
}
public enum CacheStoreMode {
USE,
BYPASS,
REFRESH
}
⇒ 캐시 모드는 EntityManager.setProperty()로 엔티티 매니저 단위로 설정하거나, EntityManager.find(), EntityManager.refresh()에 더 세밀하게 설정할 수 있음. 또는 Query.setHint()에도 사용할 수 있음.
Cache cache = emf.getCache();
boolean contains =
cache.contains(TestEntity.class, testEntity.getId());
System.out.println("contains = " + contains);
public interface Cache {
//해당 엔티티에 캐시가 있는지 여부 확인
public boolean contains(Class cls, Object primaryKey);
//해당 엔티티 중 특정 식별자를 가진 엔티티를 캐시에서 제거
public void evict(Class cls, Object primaryKey);
//해당 엔티티 전체를 캐시에서 제거
public void evict(Class cls);
//모든 캐시에서 데이터 제거
public vodi evictAll();
//JPA Cache 구현체 조회
public <T> T unwrap(Class<T> cls);
}
하이버네이트가 지원하는 캐시 3가지
**@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<ChildMember> childMembers = new ArrayList<ChildMember>();
}
@Cacheable: 엔티티를 캐시하기위해서 사용@Cache: 하이버네이트 전용 어노테이션. 캐시 관련 더 세밀한 설정을 위해 사용.@Cache| 속성 | 설명 |
|---|---|
| usage | CacheConcurrencyStrategy를 사용해 캐시 동시성 전략을 설정 |
| region | 캐시 지역 설정 |
| include | 연관 객체를 캐시에 포함할지 선택. all, non-lazy 옵션을 선택. 기본값은 all. |
| 속성 | 설명 |
|---|---|
| NONE | 캐시 설정 X |
| READ_ONLY | 읽기 전용으로 설정. 등록, 삭제는 가능하지만 수정은 불가능. |
| NONSTRICT_READ_WRITE | 엄격하지 않은 읽고 쓰기 전략. 동시에 같은 엔티티 수정 시 데이터 일관성이 깨질 수 있음. |
| READ_WRITE | 읽기 쓰기가 가능하고 READ COMMITED 정도의 격리 수준을 보장. EHCACHE는 데이터 수정하면 캐시 데이터도 같이 수정 |
| TRANSACTIONAL | 컨테이너 관리 환경에서 사용할 수 있음. 설정에 따라 REPEATABLE READ 정도의 격리 수준 보장 |
캐시를 적용한 코드는 캐시 영역에 저장됨
→ 필요시 @Cache(region = "customRegion", ...) 처럼 region 속성을 사용해 캐시 영역 직접 지정 가능
hibernate.cache.use_query_cache 옵션을 true로 설정해야함cacheable을 true로 설정em.createQuery("select i from Item i", Item.class)
.setHint("cacheable", true)
.getResultList();
쿼리 캐시 활성화 시 두 캐시 영역이 추가됨
쿼리 캐시는 캐시한 데이터 집합을 최신 데이터로 유지하려고 쿼리 캐시를 실행하는 시간과 쿼리 캐시가 사용하는 테이블들이 가장 최근에 변경된 시간을 비교함.
⇒ 쿼리 캐시는 빈번한 변경이 있는 테이블에 사용하면 성능이 저하될 수 있으니 수정이 거의 일어나지 않는 테이블에 사용해야 효과적임.
⇒ 결국 이 식별자값을 하나씩 엔티티 캐시에서 조회해 실제 엔티티를 찾음.
따라서 쿼리 캐시나 컬렉션 캐시 사용 시 결과 대상 엔티티에는 반드시 엔티티 캐시를 적용해야함