본문 바로가기
  • 실행력이 모든걸 결정한다
Spring 사전 준비/JPA Hibernate

[JPA] 트랜잭션과 Lock

by 김코더 김주역 2022. 8. 29.
반응형

1. 격리 수준

- 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 설정하는 격리의 정도

- @Transactional 어노테이션으로 격리 수준을 지정할 수 있으며, 격리 수준의 종류는 아래 포스팅의 [2. isolation]에서 확인할 수 있다.

https://kimcoder.tistory.com/477

 

[Spring] @Transactional의 속성

@Transactional은 Fallback 정책을 통해서 특정 트랜잭션 속성을 적용할 대상을 매우 유연하게 선정할 수 있도록 하는 어노테이션이다. Fallback 정책은 @Transactional 어노테이션의 작성 위치에 따른 설정

kimcoder.tistory.com

- 보통 격리수준으로 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에 정의되어 있는 락들이다.

이미지 출처 : https://kimjinyounga.github.io/server/JPA_16/

 

(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

댓글