[JPA] 트랜잭션과 락 - 3. 낙관적 락과 비관적 락

유알·2023년 3월 5일
1

[DB/JPA]

목록 보기
4/7

이전 글 -> [DB] 트랜잭션과 락 - 2. 공유락(Shared Lock) & 배타락(Exclusive Lock) 에서 이어지는 글입니다.
그리고 이 글은 일부 블로그와 김영한님의 JPA 책을 참고하여 작성되었습니다.

🍕 개요

JPA의 영속성 컨텍스트(1차 캐시)를 적절히 활용하면, 데이터베이스 트랜잭션이 READ COMMITTED 격리 수준이어도 애플리케이션 레벨에서 반복 가능한 읽기(REPEATABLE READ)가 가능하다.

김영한 님의 JPA 책에서 나온 문구이다.

즉 JPA에서 LOCK을 사용하는 이유는,
READ COMMITTED 수준의 데이터베이스에서, 일부 로직에 더 높은 격리 수준이 필요하게 되면, JPA에서 제공하는 LOCK(낙관적 락, 비관적 락) 기능을 사용하여 REPEATABLE READ가 가능하도록 하는 것이 목표이다.

또 다른 문제 : 두번의 갱신 분실문제

또 다른 문제도 있다.
만약 두 사람이 동시에 한 게시물을 수정하려고 한다고 해보자. 두사람이 순차적으로 수정 버튼을 누르게 되면 앞에 수정한 사람의 수정내용은 덮어씌워지고, 뒤에 사람의 수정내용만 더해지게 될 것이다.

이것을 두번의 갱신 분실문제 라고 한다. 이 문제는 데이터베이스 트랜잭션만으로는 해결할 수 없다.
이를 해결하기 위해서는 세가지 중 하나를 선택해야한다.

  1. 마지막 커밋만 인정하기 : 먼저 수정한 내용은 덮어 씌워진다
  2. 최초 커밋만 인정하기 : 두번째 사람이 수정버튼을 누르려고 할 때 오류 발생
  3. 충돌하는 갱신 내용 병합 : 두사람의 수정사항을 병합한다.

기본은 덮어씌워지는 마지막 커밋만 인정하기다.
이에 관련된 내용도 아래의 낙관적 락과 비관적 락에서 다뤄질 예정이다.

🍕 낙관적 락과 비관적 락의 개념

  • JPA는 데이터베이스 트랜잭션 격리 수준을 READ COMMITTED 정도로 가정한다
  • 더 높은 격리 수준이 필요하게 되면, 낙관적 락과 비관적 락 중 하나를 사용하면 된다.

그렇다면 낙관적 락과 비관적 락은 무엇일까?
타 블로그의 낙관적이다 비관적이다의 설명은 나를 더 헷갈리게 만들었다. 그래서 아래와 같이 차이점 부터 살펴보겠다.

  • 낙관적 락은 데이터베이스의 트랜잭션 락 기능을 사용하지 않고, JPA가 제공하는 버전 관리 기능을 사용한다.
  • 비관적 락은 데이터베이스의 트랜잭션 락 기능을 사용한다.

그렇다 이게 핵심이다. 여기서 살을 덧붙여가면 된다.

아래의 글은 이러한 순서로 진행된다.

  1. @Version 어노테이션
  2. 낙관적 락의 개념
  3. 비관적 락의 개념
  4. 정리

1. @Version 어노테이션

먼저 @Version 어노테이션을 알아보자
JPA가 제공하는 낙관적 락을 사용하기 위해선 엔티티에 @Version 어노테이션을 사용해서 버전관리 기능을 추가해야한다.

@Entity
public class Board {
	@Id
    private String id;
    private String title;
    
    @Version
    private Integer version;
}

이렇게 버전관리용 필드를 하나 추가하고 @Version 어노테이션을 붙여주면 된다.
적용가능 타입은 아래와 같다.

  1. Integer (int)
  2. Long (long)
  3. Short (short)
  4. Timestamp

버전 정보는 개발자가 직접 수정하면 안된다. 수정하려면 특별한 락 옵션을 선택해야한다.

2. 낙관적 락

개념

이 버전 필드는 엔티티를 수정할 때 마다 버전이 증가하게 된다.
그리고 엔티티를 수정할 때 조회시점의 버전과 수정시점의 버전이 다르면 예외가 발생하게 된다.

그러므로 JPA가 제공하는 낙관적 락은 @Version 을 사용한다.

이런식으로 이루어 지게 된다.
즉 수정할 때 마다 버전이 올라가게 되고, 수정 할때 버전 정보가 다르면 예외가 발생한다.

버전 정보 비교 방법

UPDATE BOARD
SET
	TITLE=?,
    VERSION=? (버전 증가시키기)
WHERE
	ID=?
    AND VERSION=? (where절에 버전 정보 추가)

즉 where 절에 조건을 추가한다.
만약 다른 트랜잭션이 수정해서 버전정보가 바뀐다면, 수정할 대상이 없게된다.
이때 버전이 증가한 것으로 판단해서 JPA는 예외를 발생시킨다.

낙관적 락의 특징

위의 원리를 정확히 이해하면 아래와 같은 특징이 왜 생겼는지 이해할 수 있다.

  • 엔티티가 아닌 스칼라 값을 직접 조회하면, 영속성 컨텍스트의 관리를 받지 못하므로, 반복가능한 읽기를 할 수 없다.
  • 커밋하는 시점에 충돌을 알 수 있다. (where절을 통해 감지하므로)
  • 버전(@Version)을 사용한다.
  • 두번의 갱신 분실문제 에서 첫번째 수정을 인정하는 방법을 채택한다. (두번째 수정에서 예외가 발생하므로)
  • 데이터 베이스의 트랜잭션 락을 사용하지 않고, JPA의 1차 캐시를 이용함

3. 비관적 락

개념

JPA가 제공하는 비관적 락은 데이터베이스의 트랜잭션 락 메커니즘에 의존하는 방법이다.
데이터 베이스의 트랜잭션 락에 대한 개념은 나의 이전글 을 참고하면 된다.

주로 쿼리에 select for update 구문을 사용하면서 시작하고, 버전(@Version) 정보를 사용하지 않는다.(예외 모드가 하나 있다. 아래에 설명할 예정)

비관적 락의 특징

  • 엔티티가 아닌 스칼라 값을 조회할 때도 사용가능하다.
  • 데이터를 수정하는 즉시 트랜잭션 충돌을 감지 할 수 있다.
  • 버전(@Version)을 사용하지 않는다.(예외하나 있음)
  • 두번의 갱신 분실문제 에서 첫번째 수정을 인정하는 방법을 채택한다. (두번째 수정에서 예외가 발생하므로)
  • 데이터 베이스의 트랜잭션 락을 사용함

4.정리

낙관적 락은 이름 그대로 트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법이다.
비관적 락은 트랜잭션의 충돌이 발생한다고 가정하고 데이터베이스의 트랜잭션 락을 일단 걸고 보는 방법이다.

공통점

둘다 두번의 갱신 분실 문제에서 두번째 수정에서 예외를 발생시키는 방법으로 해결한다.

차이점

낙관적 락비관적 락
엔티티 값 조회RepeatableRepeatable
스칼라 값 조회Non-RepeatableRepeatable
충돌 감지 시점커밋 할때수정 즉시
버전(@Version) 정보필요불필요(하나만 빼고)
원리JPA 영속성 컨텍스트로DB의 트랜잭션 락

외우려고 하지말고 원리를 생각하면 나머지는 이해 가능하다.

🍕 낙관적 락과 비관적 락의 적용

비관적 락의 경우 설정이 간단하다. 데이터베이스의 트랜잭션 락을 그냥 사용만 하면 되므로,
하지만 낙관적 락의 경우 버전을 어떻게 체크해야되는지 설정을 해줄 수 있다.

지금까지의 설명만 보면 이게 무슨 말인가 싶겠지만, 우리가 사용한 @Version 필드를 이용한 방법은 많은 버전 체크 옵션 중 하나 이다. 아래를 계속 읽어보자.

1. 낙관적 락의 버전 체크 방법 설정

Entity 클래스에 @OptimisticLocking 어노테이션을 이용하여 Lock설정을 할 수 있다.

@Entity
@OptimisticLocking(type = OptimisticLockType.VERSION)
@DynamicUpdate
public class Input {

    @Id
    @GeneratedValue
    private Long id;

    private String type;

    private Integer count;

    @Version
    private Long version;

}

버전을 확인하기 위한 방법인 OptimisticLockType에는 아래의 종류가 있다.

VERSION ( 기본값 )

  • 지금까지 설명한 @Version 어노테이션에 기반하여 optimistic locking을 수행합니다.
  • @Version 필드가 필요하다.
  • @OptimisticLocking 을 명시하지 않아도, @Version 이 있으면 자동으로 사용됨
  • 이것만 사용하면 UPDATE 시에만 version +1 함

ALL

  • UPDATE, DELETE SQL 문 WHERE절에 모든 필드를 제한을 줌으로써 optimistic locking을 수행합니다.
  • 처음 조회 값을 조건으로 줘서 만약 맞지 않으면 수정된 것으로 간주

DIRTY

  • UPDATE, DELETE SQL 문 WHERE에 변경된 필드를 조건을 줌으로써 optimistic locking을 수행합니다.
  • 만약 조건에 맞는 row가 없을 경우 StaleStateException 발생한다.
  • 필드에 @OprimisticLock(excluded = false)를 이용하면 해당 컬럼을 조건에서 제외시킬 수 있다.

NONE

  • optimistic locking이 @Version 어노테이션이 있어도 허용되지 않습니다.

만약 맞지 않을 시 : StaleStateException

이 어노테이션들을 사용했을 때 조건에 맞지 않아 row가 조회되지 않으면 StaleStateException 이 발생한다.

2. @Lock으로 락 설정

spring data jpa를 사용하면 간단하게 락 설정을 할 수 있다.

public interface InputRepository extends JpaRepository<Input,Long> {

    @Override
    @Lock(LockModeType.OPTIMISTIC)
    Optional<Input> findById(Long aLong);
}

Repository의 메서드에 @Lock 어노테이션을 사용한다.

여기서는 LockModeType을 주게된다.

LockModeType의 종류

JPA가 제공하는 LockModeType 옵션은 javax.persistence.LockModeType에 정의 되어있다. (jakarta.persistence.LockModeType)

락 모드타입설명
낙관적 락OPTIMISTIC낙관적 락을 사용한다
낙관적 락OPTIMISTIC_FORCE_INCREMENT낙관적 락 + 버전정보 강제 증가
비관적 락PESSIMISTIC_READ비관적락, 읽기 락
비관적 락PESSIMISTIC_WRITE비관적락, 쓰기 락
비관적 락PESSIMISTIC_FORCE_INCREMENT비관적락 + 버전정보 강제 증가
기타NONE락을 걸지 않는다.
기타READ호환을 위함(OPTIMISTIC)
기타WRITE이전버전 호환을 위해(OPTIMISTIC_FORCE_INCREMENT)

낙관적 락

예외

OptimisticLockException : 버전 정보가 맞지않았을 경우 예외

1. NONE

  • NONE을 주고 만약 Entity에 버전 설정을 했다면, 낙관적 락이 적용된다.
  • 하지만 update/delete 시에만 where절의 조건에 의해서 체크를 하므로, 수정 전에는 변경을 알아차릴 수 없다.
  • 또한 보장하는 범위 또한 조회 ~ 수정시 까지만 보장한다.
  • 즉 엔티티를 읽기만 한 경우 보장을 받을 수 없다.

2. OPTIMISTIC

public interface InputRepository extends JpaRepository<Input,Long> {

    @Override
    @Lock(LockModeType.OPTIMISTIC)
    Optional<Input> findById(Long aLong);
}

낙관적 락이다.

  • 이 경우 그저 엔티티에만 버전 정보를 설정할 때와 다르게 Read만 있는 경우에도 커밋할 때 version 정보를 확인한다.
  • 커밋 할 때 select 문이 실행되고, 이를 통해 버전 정보를 확인한다.
  • 즉 보장범위는 트랜잭션 전체가 되는 것이다.

3. OPTIMISTIC_FORCE_INCREMENT

public interface InputRepository extends JpaRepository<Input,Long> {

    @Override
    @Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
    Optional<Input> findById(Long aLong);
}

이 경우 커밋 직전 update 쿼리를 통해 version 정보를 1증가시키게 된다.

이게 무슨 의미가 있을까 싶지만, 큰 의미가 있다.
기존의 방법은 update/delete 시에만 버전이 올라갔지만 이제는 커밋 직전에 무조건 버전이 올라가게 된다.

만약 첨부파일과 게시글이 일대다 다대일 관계를 갖고 있고 첨부파일이 연관관계의 주인이라고 가정해보자.
만약 첨부파일만 추가하게되면, 게시글의 버전은 증가하지 않고 첨부파일의 버전만 올라가게 된다.
분명히 게시글의 버전도 올라가야 하지만, 게시글에는 update 문이 실행되지 않았기 때문에 버전은 올라가지 않는다.

이 경우 OPTIMISTIC_FORCE_INCREMENT 를 사용하게 되면, 게시글까지 버전이 올라가게 되므로 버전이 올라가고, 다른 트랜잭션에서 게시글이 수정되었음을 감지 할 수 있다.

정리하자면 논리적 단위의 엔티티로 버전을 관리할 수 있다.

비관적 락

PESSIMISTIC_FORCE_INCREMENT을 제외하면 버전을 사용하지 않는다.

예외

  • PessimisticLockException : 락을 얻어오지 못했을 경우
  • LockTimeoutException : 락을 기다리다가 설정한 time wait을 초과했을 경우
  • PersistanceException : NoResultException , NonUniqueResultException, LockTimeoutException 및 QueryTimeoutException을 제외한 PersistanceException 예외에 대해서는 트랜잭션에 롤백을 마킹합니다.
    chat gpt 형님의 말을 첨부한다.

1. PESSIMISTIC_WRITE

public interface InputRepository extends JpaRepository<Input,Long> {

    @Override
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional<Input> findById(Long aLong);
}
  • DB의 Exclusice Lock을 사용한다.
  • select for update를 사용

2. PESSIMISTIC_READ

public interface InputRepository extends JpaRepository<Input,Long> {

    @Override
    @Lock(LockModeType.PESSIMISTIC_READ)
    Optional<Input> findById(Long aLong);
}
  • DB의 Shared Lock을 사용하는 방법이다.
  • for share 로 Shared Lock을 획득한다.
  • 주의해야할 것은 DB에서 만약 Shared Lock을 지원하지 않으면, Exclusive Lock으로 대체한다. (Oracle 등)

3. PESSIMISTIC_FORCE_INCREMENT

public interface InputRepository extends JpaRepository<Input,Long> {

    @Override
    @Lock(LockModeType.PESSIMISTIC_FORCE_INCREMENT)
    Optional<Input> findById(Long aLong);
}
  • DB의 Exclusive Lock 방식을 사용하면서 낙관적 락처럼 Version 필드도 사용한다.
  • 하이버네이트는 nowait을 지원하는 db에서는 for update nowait 사용 (mysql 8.0 이상 등등)
  • nowait 키워드는 어떤 락이 걸려 있던 간에, 대기하지 않고 바로 오류를 발생시킨다.
  • Read만 하는 경우도 커밋 직전에 버전을 올린다.

3. entityManager 락 설정

락 적용

EntityManager.lock(), EntityManager.find(), EntityManager.refresh()
Query.setLockMode() //이건 참고 entityManager은 아니지만
@NamedQuery //이건 참고 entityManager은 아니지만

예시

바로 적용

Board board = em.find(Board.class, id, LockModeType.OPTIMISTIC);

필요할 때 락 적용

Board board = em.find(Board.class, id);
//...
em.lock(LockModeType.OPTIMISTIC);

PESSIMISTIC 락의 Lock scope 설정

이 부분은 Spring data jpa 처럼 간단하게 어노테이션으로 설정하는 방법을 찾지 못했다.
혹시 아는 분들은 댓글 부탁드립니다.

entityManager.find(Student.class, studentId, LockModeType.PESSIMISTIC_READ);
  • PessimisticLockScope.NORMAL
    기본값으로써 해당 entity만 잠금이 설정됩니다.
    @Inheritance(strategy = InheritanceType.JOINED)와 같이 조인 상속을 사용하면 부모도 함께 잠금이 설정됩니다.

  • PessimisticLockScope.EXTENDED
    @ElementCollection, @OneToOne, @OneToMany 등 연관된 entity들도 잠금이 설정됩니다.

profile
더 좋은 구조를 고민하는 개발자 입니다

0개의 댓글