Backend/SpringBoot-JPA

Optimistic/Pessimistic Lock

Jayyy.H 2021. 4. 19. 22:49

LOCK

OPTIMISTIC LOCK

  • 트랜잭션의 대부분은 충돌이 발생하지 않는다는 것을 가정하고 접근하는 방법으로 트랜잭션이 커밋하기 전까지는 충돌 여부를 알 수 없다.
  • JPA에서 OPTIMISTIC LOCK을 @Version을 사용하므로 Entity 정의 시 @Version이 명시된 필드를 추가하도록 하자. 또한, @Version이 정의되어 있다면 OPTIMISTIC LOCK을 사용할 것을 명시하지 않더라도 DEFAULT 적용 된다.
  • 이러한 OPTIMISTIC LOCK은 COMMIT 시점에 충돌을 알 수 있으며 충돌 시 다음과 같은 Exception이 발생한다.
    • javax.persistence.OptimisticLockException in JPA
    • org.hibernate.StaleObjectStateException in Hibernate
    • org.springframework.orm.ObjectOptimisticLockingFailureException in Spring

@Version

  • OPTIMISTIC LOCK 사용 시 @Version 어노테이션을 이용해 버전관리 기능을 추가해야 한다. 이는 Long, Integer, Short, Timestamp 타입에 적용 가능하다.
    • @Version
      private Long version;
  • 엔티티를 수정할 때 마다 버전이 하나씩 자동으로 증가하며, 수정 시 조회 시점의 버전과 수정 시점의 버전이 다르면 예외를 발생시킨다. 이를 통해 second lost update problem최초 커밋만 인정함으로써 방지할 수 있다. 다른 방법으로는 마지막 커밋만 인정, 충돌 내용 병합이 있으나 여기서 다룰 문제는 아니다.
    • tx-1> Member1 조회 (version 1)
    • tx-2> Member1 조회 (version 1)
    • tx-2> Member1 수정 (version 1->2)
    • tx-1> Member1 수정 (충돌 발생)
  • 엔티티 수정 시 다음과 같은 UPDATE 쿼리가 발생된다.
    • UPDATE MEMBER
      SET
         NAME=${NEW_NAME},
         VERSION=${OLD_VERSION + 1} 
      WHERE
         ID=${ID}
         AND VERSION=${OLD_VERSION}
    • 이러한 버전관리 필드는 JPA가 직접 관리하는데, 벌크 연산시에는 개발자가 관리해 주어야 한다.
    • UPDATE MEMBER m SET m.name = ${NEW_NAME}, m.version = m.version + 1

PESSIMISTIC LOCK

  • 트랜잭션의 충돌이 발생한다고 가정하고 우선 LOCK을 걸고 보는 방법이다.
  • SELECT FOR UPDATE, `` 등의 방법이 있다.
  • 데이터를 수정하는 즉시 충돌여부를 확인할 수 있으며, 다음과 같은 Exception이 발생한다.
    • javax.persistence.PessimisticLockException in JPA
    • org.springframework.dao.PessimisticLockingFailureException in Spring
  • PESSIMISTIC LOCK을 얻은 트랜잭션이 LOCK을 풀기 전까지 다른 트랜잭션들은 무한히 대기하게 되므로 다음과같이 timeout을 설정하는 것이 좋다.
    • Map<String, Object> properties = new HashMap<String, Object>();
      // timeout for 10 seconds
      properties.put("javax.persistence.lock.timeout", 10 * 1000);
      
      Member member = em.find(Member.class, id, LockModeType.PESSIMISTIC_WRITE, properties);
  • PessimisticLockScope를 통해 해당 Entity만 Lock을 걸지, 연관관계에 있는 Entity들도 Lock을 걸지 결정할 수 있다.
    • PessimisticLockScope.NORMAL: 해당 Entity만 Lock을 건다.
    • PessimisticLockScope.EXTENDED: 연관된 Entity들도 Lock이 걸린다.

JPA Lock

  • JPA 사용 시, READ COMMITTED + OPTIMISTIC LOCK을 조합하여 사용하는 것이 좋다.
  • Lock을 사용하는 대표적인 예는
    • Entity Manager: Member foundMember = em.find(Member.class, id, LockModeType.OPTIMISTIC);, em.lock(foundMember, LockModeType.OPTIMISTIC);, em.refresh()
    • Query: em.createQuery("select ~").setLockMode(LockModeTpye.OPTIMISTIC_INCREMENT);
    • @NamedQuery(name="lock", query="SELECT ~~", lockMode = OPTIMISTIC_INCREMENT)
      가 있다.

LockModeType의 종류

Lock Mode Type 설명
OPTIMISTIC LOCK OPTIMISTIC - OPTIMISTIC LOCK을 사용한다.
- @Version만 적용했을때에는 Entity 수정 시에만 버전을 확인하지만, 여기에서는 조회만 하여도 버전을 체크함으로써 트랜잭션이 종료될 때 까지 다른 트랜잭션에서 변경하지 않음을 보장해준다.
- 트랜잭션 커밋 시 버전정보를 조회(SELECT)해 Entity의 버전과 같은지 확인한다.
- DIRTY READ, NON-REPEATABLE READ를 방지한다.
OPTIMISTIC LOCK OPTIMISTIC_FORCE_INCREMENT - OPTIMISTIC LOCK과 함께 버전정보를 강제로 증가한다.
- Entity를 수정하지 않아도 트랜잭션 커밋 시 버전정보를 강제로 증가시킨다. 이 때, 조회시점의 버전과 다르면 Exception을 발생시킨다.
- Entity 수정 시 버전 UPDATE가 발생하는데, 트랜잭션 커밋 시점에도 버전정보가 증가하므로 2번의 버전 증가가 발생할 수 있다.
- 연관관계가 있는 Entity들에 대해 연관관계의 주인인 Entity의 변경이 발생하면 연관관계에 있는 Entity의 버전정보도 동일하게 변경된다. (Member 변경 시 Team 또한 Version이 같이 업데이트)
PESSIMISTIC LOCK PESSIMISTIC_READ - PESSIMISTIC LOCK, READ LOCK을 사용한다.
- 데이터를 읽기만하고 수정하지 않는 용도로 사용한다.
- shared lock으로 select ... for share와 같다.
PESSIMISTIC LOCK PESSIMISTIC_WRITE - PESSIMISTIC LOCK, WRITE LOCK을 사용한다.
- Lock이 걸린 row는 다른 트랜잭션이 수정할 수 없다.
- 내부적으로 select ... for update를 사용해 lock을 건다.
- exclusive lock
PESSIMISTIC LOCK PESSIMISTIC_FORCE_INCREMENT - PESSIMISTIC LOCK, 버전정보를 강제로 증가한다.
- PESSIMISTIC LOCK 중 유일하게 버전정보를 사용한다.
- for update nowait을 사용한다.
기타 NONE - LOCK을 사용하지 않는다.
- @Version필드가 있으면 OPTIMISTIC LOCK이 적용된다.
- Entity 수정 시, 버전 필드를 체크하면서 버전을 증가시킨다.