부끄럽지만 JPA를 사용하면서도 어노테이션을 통해 잠금이 가능하다는걸 알지 못했다. 그래서 이것들을 정리해보고자 한다.
현실적으로 데이터 갱신 시 경합이 발생하지 않을 것이라고 보고 잠금을 거는 기법이다. 예를 들어서 회원 정보 수정과 같은 경우는 회원 본인에 의해서 수정이 이루어지기 때문에 동시에 여러 요청이 발생할 가능성이 낮다. 따라서 수정이 이루어진 경우를 감지해서 예외를 발생시켜도 실제로 예외가 발생할 가능성이 낮다고 낙관적으로 보는 것이다. 엄밀한 의미에서 잠금보다 충돌감지(Conflict Detection)에 가깝다.
동일한 데이터를 동시에 수정할 가능성이 높다는 관점으로 잠금을 거는 기법이다. 예를 들어서 상품의 재고는 동시에 같은 상품을 여러명이 주문할 수 있기 때문에 데이터 수정에 의한 경합이 발생할 가능성이 높다고 비관적으로 보는 것이다. 이 경우 충돌감지를 통해 잠금을 발생시키면 충돌발생에 의해서 예외가 자주 발생한다. 이럴 경우 비관적 락을 통해서 예외를 발생시키지 않고, 정합성을 보장하는 것이 가능하다. 다만 성능적 측면에서 손실을 감수해야 한다. 주로 DB에서 제공하는 배타적 잠금(Exclusive Lock)을 사용한다.
비관적 락에는 공유 락(Shared Lock)과 배타적 락(Exclusive Lock)이 있다.
- Shared Lock : 데이터를 동시에 Read 하는 것은 가능하지만, Write 는 불가능
- Exclusive Lock : 트랜잭션이 끝나는 시간동안 Read/Write 불가
버전 관리를 위한 @Version
필드에는 int, Integer, short, Short, long, Long, TimeStamp
타입을 사용할 수 있다.
@Entity
public class Member {
@Id
private Long id;
private String name;
@Version
private Integer version;
}
JPA 에서 낙관적 락을 사용하기 위해서는 엔티티 클래스에 버전 관리를 위한 필드를 추가해야 한다. @Version
어노테이션을 통해서 엔티티가 수정될 때마다 자동으로 버전을 증가시키고, 커밋을 하기 전 엔티티의 버전과 DB의 버전이 같은지를 확인한다. 버전이 다르면 ObjectOptimisticLockingFailureException
을 발생시킨다.
em.find(Member.class,id,LockModeType.OPTIMISTIC)
추가적으로 JPA는 데이터 조회문에 락을 걸 수 있는데, LockModeType.OPTIMISTIC
를 사용하면 격리 수준을 더 향상시킬 수 있다. @Version
만 사용하는 경우 데이터를 수정하는 경우에만 버전이 체크 되지만, LockModeType.OPTIMISTIC
를 사용하면 단순조회 시에도 버전 관리가 일어난다.
JpaRepository
를 사용하는 경우 @Lock
어노테이션을 통해서 지정할 수 있음
OPTIMISTIC_FORCE_INCREMENT
를 사용하거나, 자식 엔티티 수정시 변경할 필드를 추가해야만 함(ex. 자식 엔티티 수정일자)Member member = entityManager.find(Member.class, memberNo, LockModeType.PESSIMISTIC_READ);
다른 트랜잭션에게 읽기만을 허용하는 LockModeType
이다. Shared Lock
을 이용해 락을 거는데 Shared Lock
을 DB가 제공하지 않으면 PESSIMISTIC_WRITE
와 동일하게 동작한다.
Member member = entityManager.find(Member.class, memberNo, LockModeType.PESSIMISTIC_WRITE);
DB에서 제공하는 행 배타잠금(Row Exclusive Lock)을 이용해 잠금을 획득한다. 다른 트랜잭션에서 쓰지도 읽지도 못한다.
Member member = entityManager.find(Member.class, memberNo, LockModeType.PESSIMISTIC_FORCE_INCREMENT);
DB에서 제공하는 행 배타잠금(Row Exclusive Lock)을 이용해 잠금을 걺과 동시에 버전을 증가시킨다. 해당하는 엔티티에 변경은 없지만 하위 엔티티 갱신을 위해 잠금이 필요한 경우 사용할 수 있다.
일반적으로 낙관적 락은 처리 요청을 받은 순간부터 처리가 종료될 때까지 레코드를 잠그는 비관적 락보다 성능이 좋다. 하지만 낙관적 락은 커밋을 하는 시점에 충돌 여부를 알 수 있기 때문에 상황에 따라 비관적 락보다 성능이 안좋을 수도 있다.
만약 재고가 1개가 있는 상품이 있고, 이를 사려는 사용자가 10명이 있다고 가정하자.
비관적 락
의 경우, 트랜잭션에서 충돌 여부를 파악하기 때문에 재고 없음을 미리 알 수 있기 때문에 복잡한 처리를 하지 않아도 된다.
낙관적 락
의 경우, 10명이 동시에 처리를 하다가 커밋을 하려는 순간이 되어서야 재고가 없음을 알 수 있다. 처리가 진행된만큼 롤백을 진행해야 하고, 그만큼 많은 리소스가 소요된다.