[JPA] 잠금의 종류

신명철·2022년 9월 12일
0

JPA

목록 보기
10/14

들어가며

부끄럽지만 JPA를 사용하면서도 어노테이션을 통해 잠금이 가능하다는걸 알지 못했다. 그래서 이것들을 정리해보고자 한다.

Locking 종류

1. 낙관적 락(Optimistic Lock)

현실적으로 데이터 갱신 시 경합이 발생하지 않을 것이라고 보고 잠금을 거는 기법이다. 예를 들어서 회원 정보 수정과 같은 경우는 회원 본인에 의해서 수정이 이루어지기 때문에 동시에 여러 요청이 발생할 가능성이 낮다. 따라서 수정이 이루어진 경우를 감지해서 예외를 발생시켜도 실제로 예외가 발생할 가능성이 낮다고 낙관적으로 보는 것이다. 엄밀한 의미에서 잠금보다 충돌감지(Conflict Detection)에 가깝다.

  • 장점
    • 동시 요청에 대해 처리 성능이 좋다
  • 단점
    • 충돌이 자주 일어나면 롤백이 자주 일어나 비용이 많이 들어 오히려 성능상 손해를 볼 수 있다
    • 롤백 처리를 구현하는게 복잡할 수 있다

2. 비관적 락(Pessimistic Lock)

동일한 데이터를 동시에 수정할 가능성이 높다는 관점으로 잠금을 거는 기법이다. 예를 들어서 상품의 재고는 동시에 같은 상품을 여러명이 주문할 수 있기 때문에 데이터 수정에 의한 경합이 발생할 가능성이 높다고 비관적으로 보는 것이다. 이 경우 충돌감지를 통해 잠금을 발생시키면 충돌발생에 의해서 예외가 자주 발생한다. 이럴 경우 비관적 락을 통해서 예외를 발생시키지 않고, 정합성을 보장하는 것이 가능하다. 다만 성능적 측면에서 손실을 감수해야 한다. 주로 DB에서 제공하는 배타적 잠금(Exclusive Lock)을 사용한다.

  • 장점
    • 충돌이 잦은 환경에서는 롤백을 줄일 수 있어 성능상 유리하다
    • 데이터 무결성을 보장할 수 있다
  • 단점
    • 레코드 자체에 락을 걸기 때문에 동시 처리 시 성능상 손해를 볼 수 있다
    • 서로 자원이 필요한 경우 데드락이 일어날 가능성이 존재한다

비관적 락에는 공유 락(Shared Lock)과 배타적 락(Exclusive Lock)이 있다.

  • Shared Lock : 데이터를 동시에 Read 하는 것은 가능하지만, Write 는 불가능
  • Exclusive Lock : 트랜잭션이 끝나는 시간동안 Read/Write 불가

낙관적 락 사용 방법

@Version 사용

버전 관리를 위한 @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을 발생시킨다.

  • 예를 들어서, A가 회원을 수정 중 B가 회원을 수정해서 커밋하면 A가 커밋하는 순간 DB와 버전이 달라 예외가 발생한다.
em.find(Member.class,id,LockModeType.OPTIMISTIC)

추가적으로 JPA는 데이터 조회문에 락을 걸 수 있는데, LockModeType.OPTIMISTIC를 사용하면 격리 수준을 더 향상시킬 수 있다. @Version만 사용하는 경우 데이터를 수정하는 경우에만 버전이 체크 되지만, LockModeType.OPTIMISTIC를 사용하면 단순조회 시에도 버전 관리가 일어난다.

LockModeType 종류

JpaRepository를 사용하는 경우 @Lock 어노테이션을 통해서 지정할 수 있음

OPTIMISTIC

  • 트랜잭션 시작 시 버전 점검이 수행되고, 트랜잭션 종료 시에도 버전 점검이 수행됨(커밋 직전 버전 확인 쿼리 한번 더 수행)
  • 엔티티에 대한 변경이 없다면 암시적 배타잠금(Row Exclusive Lock)이 발생하지 않기 때문에 자식 엔티티 수정이 필요한 경우 빈틈이 존재함. OPTIMISTIC_FORCE_INCREMENT 를 사용하거나, 자식 엔티티 수정시 변경할 필드를 추가해야만 함(ex. 자식 엔티티 수정일자)

OPTIMISTIC_FORCE_INCREMENT

  • 낙관적 락을 사용하면서 추가로 버전을 강제로 증가시키는 방법(커밋 직전에 버전만 증가시키는 쿼리가 항상 발행된다)
  • 변경사항이 있는 경우 버전 증가 UPDATE문에 의해 두번 버전이 증가하게 되기 때문에 엔티티 자체에 변경사항이 있는 경우 불필요한 UPDATE문이 발행되지 않도록 주의 필요
  • 암시적인 행 배타잠금 (Row Exclusive Lock)이 발생되어 정합성을 보증할 수 있기 때문에 자식 엔티티를 수정할 때 자식 엔티티 전체에 대한 잠금 용도로 사용할 수 있음

비관적 잠금 사용 방법

PESSIMISTIC_READ

Member member = entityManager.find(Member.class, memberNo, LockModeType.PESSIMISTIC_READ);

다른 트랜잭션에게 읽기만을 허용하는 LockModeType이다. Shared Lock을 이용해 락을 거는데 Shared Lock을 DB가 제공하지 않으면 PESSIMISTIC_WRITE와 동일하게 동작한다.

PESSIMISTIC_WRITE

Member member = entityManager.find(Member.class, memberNo, LockModeType.PESSIMISTIC_WRITE);

DB에서 제공하는 행 배타잠금(Row Exclusive Lock)을 이용해 잠금을 획득한다. 다른 트랜잭션에서 쓰지도 읽지도 못한다.

PESSIMISTIC_FORCE_INCREMENT

Member member = entityManager.find(Member.class, memberNo, LockModeType.PESSIMISTIC_FORCE_INCREMENT);

DB에서 제공하는 행 배타잠금(Row Exclusive Lock)을 이용해 잠금을 걺과 동시에 버전을 증가시킨다. 해당하는 엔티티에 변경은 없지만 하위 엔티티 갱신을 위해 잠금이 필요한 경우 사용할 수 있다.

비교

일반적으로 낙관적 락은 처리 요청을 받은 순간부터 처리가 종료될 때까지 레코드를 잠그는 비관적 락보다 성능이 좋다. 하지만 낙관적 락은 커밋을 하는 시점에 충돌 여부를 알 수 있기 때문에 상황에 따라 비관적 락보다 성능이 안좋을 수도 있다.

만약 재고가 1개가 있는 상품이 있고, 이를 사려는 사용자가 10명이 있다고 가정하자.
비관적 락의 경우, 트랜잭션에서 충돌 여부를 파악하기 때문에 재고 없음을 미리 알 수 있기 때문에 복잡한 처리를 하지 않아도 된다.
낙관적 락의 경우, 10명이 동시에 처리를 하다가 커밋을 하려는 순간이 되어서야 재고가 없음을 알 수 있다. 처리가 진행된만큼 롤백을 진행해야 하고, 그만큼 많은 리소스가 소요된다.


profile
내 머릿속 지우개

0개의 댓글