이전 글 -> [DB] 트랜잭션과 락 - 2. 공유락(Shared Lock) & 배타락(Exclusive Lock) 에서 이어지는 글입니다.
그리고 이 글은 일부 블로그와 김영한님의 JPA 책을 참고하여 작성되었습니다.
JPA의 영속성 컨텍스트(1차 캐시)를 적절히 활용하면, 데이터베이스 트랜잭션이 READ COMMITTED 격리 수준이어도 애플리케이션 레벨에서 반복 가능한 읽기(REPEATABLE READ)가 가능하다.
김영한 님의 JPA 책에서 나온 문구이다.
즉 JPA에서 LOCK을 사용하는 이유는,
READ COMMITTED 수준의 데이터베이스에서, 일부 로직에 더 높은 격리 수준이 필요하게 되면, JPA에서 제공하는 LOCK(낙관적 락, 비관적 락) 기능을 사용하여 REPEATABLE READ가 가능하도록 하는 것이 목표이다.
또 다른 문제도 있다.
만약 두 사람이 동시에 한 게시물을 수정하려고 한다고 해보자. 두사람이 순차적으로 수정 버튼을 누르게 되면 앞에 수정한 사람의 수정내용은 덮어씌워지고, 뒤에 사람의 수정내용만 더해지게 될 것이다.
이것을 두번의 갱신 분실문제 라고 한다. 이 문제는 데이터베이스 트랜잭션만으로는 해결할 수 없다.
이를 해결하기 위해서는 세가지 중 하나를 선택해야한다.
기본은 덮어씌워지는 마지막 커밋만 인정하기다.
이에 관련된 내용도 아래의 낙관적 락과 비관적 락에서 다뤄질 예정이다.
그렇다면 낙관적 락과 비관적 락은 무엇일까?
타 블로그의 낙관적이다 비관적이다의 설명은 나를 더 헷갈리게 만들었다. 그래서 아래와 같이 차이점 부터 살펴보겠다.
그렇다 이게 핵심이다. 여기서 살을 덧붙여가면 된다.
아래의 글은 이러한 순서로 진행된다.
먼저 @Version 어노테이션을 알아보자
JPA가 제공하는 낙관적 락을 사용하기 위해선 엔티티에 @Version 어노테이션을 사용해서 버전관리 기능을 추가해야한다.
@Entity
public class Board {
@Id
private String id;
private String title;
@Version
private Integer version;
}
이렇게 버전관리용 필드를 하나 추가하고 @Version 어노테이션을 붙여주면 된다.
적용가능 타입은 아래와 같다.
버전 정보는 개발자가 직접 수정하면 안된다. 수정하려면 특별한 락 옵션을 선택해야한다.
이 버전 필드는 엔티티를 수정할 때 마다 버전이 증가하게 된다.
그리고 엔티티를 수정할 때 조회시점의 버전과 수정시점의 버전이 다르면 예외가 발생하게 된다.
그러므로 JPA가 제공하는 낙관적 락은 @Version 을 사용한다.
이런식으로 이루어 지게 된다.
즉 수정할 때 마다 버전이 올라가게 되고, 수정 할때 버전 정보가 다르면 예외가 발생한다.
UPDATE BOARD
SET
TITLE=?,
VERSION=? (버전 증가시키기)
WHERE
ID=?
AND VERSION=? (where절에 버전 정보 추가)
즉 where 절에 조건을 추가한다.
만약 다른 트랜잭션이 수정해서 버전정보가 바뀐다면, 수정할 대상이 없게된다.
이때 버전이 증가한 것으로 판단해서 JPA는 예외를 발생시킨다.
위의 원리를 정확히 이해하면 아래와 같은 특징이 왜 생겼는지 이해할 수 있다.
JPA가 제공하는 비관적 락은 데이터베이스의 트랜잭션 락 메커니즘에 의존하는 방법이다.
데이터 베이스의 트랜잭션 락에 대한 개념은 나의 이전글 을 참고하면 된다.
주로 쿼리에 select for update 구문을 사용하면서 시작하고, 버전(@Version) 정보를 사용하지 않는다.(예외 모드가 하나 있다. 아래에 설명할 예정)
낙관적 락은 이름 그대로 트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법이다.
비관적 락은 트랜잭션의 충돌이 발생한다고 가정하고 데이터베이스의 트랜잭션 락을 일단 걸고 보는 방법이다.
둘다 두번의 갱신 분실 문제에서 두번째 수정에서 예외를 발생시키는 방법으로 해결한다.
낙관적 락 | 비관적 락 | |
---|---|---|
엔티티 값 조회 | Repeatable | Repeatable |
스칼라 값 조회 | Non-Repeatable | Repeatable |
충돌 감지 시점 | 커밋 할때 | 수정 즉시 |
버전(@Version) 정보 | 필요 | 불필요(하나만 빼고) |
원리 | JPA 영속성 컨텍스트로 | DB의 트랜잭션 락 |
외우려고 하지말고 원리를 생각하면 나머지는 이해 가능하다.
비관적 락의 경우 설정이 간단하다. 데이터베이스의 트랜잭션 락을 그냥 사용만 하면 되므로,
하지만 낙관적 락의 경우 버전을 어떻게 체크해야되는지 설정을 해줄 수 있다.
지금까지의 설명만 보면 이게 무슨 말인가 싶겠지만, 우리가 사용한 @Version 필드를 이용한 방법은 많은 버전 체크 옵션 중 하나 이다. 아래를 계속 읽어보자.
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에는 아래의 종류가 있다.
이 어노테이션들을 사용했을 때 조건에 맞지 않아 row가 조회되지 않으면 StaleStateException 이 발생한다.
spring data jpa를 사용하면 간단하게 락 설정을 할 수 있다.
public interface InputRepository extends JpaRepository<Input,Long> {
@Override
@Lock(LockModeType.OPTIMISTIC)
Optional<Input> findById(Long aLong);
}
Repository의 메서드에 @Lock
어노테이션을 사용한다.
여기서는 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 : 버전 정보가 맞지않았을 경우 예외
public interface InputRepository extends JpaRepository<Input,Long> {
@Override
@Lock(LockModeType.OPTIMISTIC)
Optional<Input> findById(Long aLong);
}
낙관적 락이다.
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을 제외하면 버전을 사용하지 않는다.
public interface InputRepository extends JpaRepository<Input,Long> {
@Override
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Input> findById(Long aLong);
}
public interface InputRepository extends JpaRepository<Input,Long> {
@Override
@Lock(LockModeType.PESSIMISTIC_READ)
Optional<Input> findById(Long aLong);
}
public interface InputRepository extends JpaRepository<Input,Long> {
@Override
@Lock(LockModeType.PESSIMISTIC_FORCE_INCREMENT)
Optional<Input> findById(Long aLong);
}
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);
이 부분은 Spring data jpa 처럼 간단하게 어노테이션으로 설정하는 방법을 찾지 못했다.
혹시 아는 분들은 댓글 부탁드립니다.
entityManager.find(Student.class, studentId, LockModeType.PESSIMISTIC_READ);
PessimisticLockScope.NORMAL
기본값으로써 해당 entity만 잠금이 설정됩니다.
@Inheritance(strategy = InheritanceType.JOINED)와 같이 조인 상속을 사용하면 부모도 함께 잠금이 설정됩니다.
PessimisticLockScope.EXTENDED
@ElementCollection, @OneToOne, @OneToMany 등 연관된 entity들도 잠금이 설정됩니다.