1. 격리 수준
- 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 설정하는 격리의 정도
- @Transactional 어노테이션으로 격리 수준을 지정할 수 있으며, 격리 수준의 종류는 아래 포스팅의 [2. isolation]에서 확인할 수 있다.
https://kimcoder.tistory.com/477
- 보통 격리수준으로 READ COMMITTED을 기본으로 사용한다.
- 일부 중요한 비즈니스 로직에 더 높은 격리 수준이 필요하면 lock 기능을 사용하면 된다.
※ Lock 대신 MVCC(Multiversion Concurrency Control)을 사용하는 DB들도 있다.
2. Second Lost Update Problem
- 특정 데이터를 두 사용자가 동시에 수정하려고 할 때, 먼저 수정한 내용은 분실되는 문제다.
- 트랜잭션의 범위를 넘어서는 문제다.
- 최초 커밋만 인정하거나, 마지막 커밋만 인정하거나(기본적으로 사용됨), 충돌하는 갱신 내용을 병합하는 식으로 해결해야 한다. JPA가 제공하는 버전 관리 기능을 사용하면 최초 커밋만 인정하도록 구현할 수 있다.
3. @Version
- 버전 관리 기능을 추가할 때 사용되는 어노테이션이다.
- 최초 커밋만 인정하도록 구현할 수 있다.
1) 적용 가능 타입
- Long(long), Integer(int), Short(short), Timestamp
2) 적용 방법
- 엔티티에 버전 관리용 필드를 만들고 @Version 어노테이션을 붙인다.
@Version
private Integer version;
3) 동작 원리
- 엔티티를 수정할 때마다 버전이 하나씩 자동으로 증가하게 된다.
- 조회 시점의 버전과 수정 시점의 버전이 같다면 UPDATE 쿼리가 실행될 때 버전값도 같이 증가하고, 조회 시점의 버전과 수정 시점의 버전이 다르다면 예외가 발생한다. 예를 들어, 조회 시점의 버전은 1인데 다른 트랜잭션에서 동일한 데이터를 수정해서 버전이 2로 늘어났다면 수정을 시도 했을 때 예외가 발생하는 것이다.
- 임베디드 타입과 값 타입 컬렉션도 논리적인 개념상 엔티티의 값이므로 수정되었을 때 버전이 증가한다.
4) 주의할 점
- @Version으로 추가한 버전 관리 필드는 JPA가 직접 관리하기 때문에 임의로 수정하지 않는 것이 좋다. 단, 다음과 같이 커밋만으로 버전 필드를 강제로 증가시켜야 하는 경우가 있다.
- 벌크 연산은 버전을 무시하기 때문에 벌크 연산에서 버전을 증가하려면 버전 필드를 강제로 증가시켜야 한다.
- 연관관계 필드는 외래 키를 관리하는 연관관계의 주인 필드를 수정할 때만 버전이 증가한다.
- 일대다/다대일 양방향 연관관계에서 단순히 다(N)쪽의 엔티티를 추가하는 것만으로 일(1)쪽의 버전은 증가하지 않는다.
※ 버전 값을 강제로 증가하려면 특별한 락 옵션을 선택해야 한다. [5-2) JPA 락의 종류]를 참고하자.
4. 낙관적 락과 비관적 락의 의미
1) 낙관적 락
- 트랜잭션 대부분은 충돌이 발생하지 않는다고 가정하는 방법이다.
- DB Lock을 사용하지 않고 JPA가 제공하는 버전 관리 기능을 사용한다. 즉, 애플리케이션이 제공하는 락을 사용하는 것이다.
- 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다.
2) 비관적 락
- 트랜잭션의 충돌이 발생한다고 가정하고 먼저 락을 걸고 보는 방법이다.
- DB Lock을 사용한다.
5. JPA 락 사용
1) Lock 적용 가능 위치
- EntityManager의 lock(), find(), refresh()
- Query 또는 TypedQuery의 setLockMode()
- @NamedQuery
적용 예시
// EntityManager.find() 이용
Member member = em.find(Member.class, id, LockModeType.OPTIMISTIC); // 낙관적 락
// EntityManager.lock() 이용
Member member = em.find(Member.class, id);
em.lock(member, LockModeType.OPTIMISTIC);
2) JPA의 낙관적 락과 비관적 락
(1) JPA의 낙관적 락
- @Version만 있어도 낙관적 락이 적용된다.
- 트랜잭션을 커밋하는 시점에 충돌을 알 수 있다는 장점이 있다.
- 낙관적 락에서 발생하는 예외는 다음과 같다.
- javax.persistence.OptimisticLockException
- org.hibernate.StaleObjectStateException
- org.springframework.orm.ObjectOptimisticLockingFailureException
(2) JPA의 비관적 락
- DB Lock에 의존한다.
- 주로 SQL 쿼리에 select for update 구문을 사용하면서 시작하고 버전 정보는 사용하지 않는다.
- 보통은 잠시 후에 살펴볼 PESSIMISTIC_WRITE 모드를 사용한다.
- 엔티티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있다.
- 데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다.
- 비관적 락에서 발생하는 예외는 다음과 같다.
- javax.persistence.PessimisticLockException
- org.springframework.dao.PessimisticLockingFailureException
3) JPA 락의 종류
- javax.persistence.LockModeType에 정의되어 있는 락들이다.
(1) NONE
- 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경 및 삭제되지 않아야 할 때 사용한다.
- 일반적인 @Version의 동작이다.
(2) OPTIMISTIC
- @Version만 적용했을 때와 달리 엔티티를 조회만 해도 버전을 체크한다.
- 한 번 조회한 엔티티는 트랜잭션을 종료할 때까지 다른 트랜잭션에서 변경하지 않음을 보장한다.
- 트랜잭션을 커밋할 때 SELECT 쿼리로 DB에 있는 버전을 조회해서 검증한다.
(3) OPTIMISTIC_FORCE_INCREMENT
- 낙관적 락을 사용하면서, 커밋할 때 버전 정보를 강제로 증가시킨다.
- 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해서 버전을 강제로 증가시킨다. 추가로 엔티티를 수정한다면 2번의 버전 증가가 나타날 수 있다. 물론, 엔티티와 DB의 버전이 다르면 예외가 발생한다.
(4) PESSIMISTIC_WRITE
- 일반적으로 사용되는 비관적 락으로, DB에 쓰기 락을 걸 때 사용한다.
- select for update 구문을 사용해서 DB 락을 건다.
- 락이 걸린 로우는 다른 트랜잭션이 수정할 수 없다.
(5) PESSIMISTIC_READ
- 데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용한다.
- 보통 잘 사용되지 않는다.
(6) PESSIMISTIC_FORCE_INCREMENT
- 비관적 락을 사용하면서, 커밋할 때 버전 정보를 강제로 증가시킨다.
- Hibernate는 nowait를 지원하는 DB에 대해서는 for update nowait 옵션을 적용하며, nowait를 지원하지 않는 DB에 대해서는 for update를 적용한다.
4) JPA 추천 전략
- JPA를 사용할 때는 READ COMMITTED 트랜잭션 격리 수준과 낙관적 버전 관리를 사용하는 것이 좋다.
6. 비관적 락과 타임 아웃
- 비관적 락을 사용하는 경우에 트랜잭션이 락을 획득할 때까지 대기하는 시간을 한정할 수 있다.
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("javax.persistence.lock.timeout", 5000); // 5000ms=5초
Member mebmer = em.find(Member.class, id, LockModeType.PESSIMISTIC_WRITE, properties);
- 정해진 시간동안 응답이 없으면 javax.persistence.LockTimeoutException 예외가 발생한다.
- DB 특성에 따라 동작하지 않을 수도 있다.
● 참고자료 : 자바 ORM 표준 JPA 프로그래밍
'Spring 사전 준비 > JPA Hibernate' 카테고리의 다른 글
[JPA] LazyInitializationException: could not initialize proxy 해결 (0) | 2022.12.28 |
---|---|
[JPA] 2차 캐시 (0) | 2022.08.30 |
[JPA] 성능 최적화 (0) | 2022.08.27 |
[JPA] JPA 예외 처리 (0) | 2022.08.25 |
[JPA] 엔티티 그래프 (0) | 2022.08.25 |
댓글