/////
Search
Duplicate
2️⃣

선점 잠금

: 선점 잠금은 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드들이 해당 애그리거트를 수정하지 못하도록 막는 방식
스레드1에서 선점 잠금 방식으로 애그리거트를 구한 뒤 이어서 스레드2가 같은 애그리거트를 구하고 있다. 이때 스레드2는 스레드1이 애그리거트에 대한 잠금을 해제할 때까지 블로킹된다.
스레드1이 애그리거트를 수정하고 트랜잭션을 커밋하면 잠금을 해제한다. 이 순간 대기하고 있던 스레드2가 애그리거트에 접근하게 된다.
스레드1이 트랜잭션을 커밋한 뒤에 스레드2가 애그리거트를 구하게 되므로 스레드2는 스레드1이 수정한 애그리거트의 내용을 보게된다.
: 한 스레드가 애그리거트를 구하고 수정하는 동안 다른 스레드가 수정할 수 없으므로 동시에 애그리거트를 수정할 때 발생하는 데이터 충돌 문제를 해소할 수 있다.
: 앞서 배송지 정보 수정과 배송 상태 변경을 동시에 수행하는 문제에 선점 잠금을 작용하면 다음과 같이 작동한다.
운영자 스레드가 먼저 선점 잠금 방식으로 주문 애그리거트를 구하면 운영자 스레드가 잠금을 해제할 때까지 고객 스레드는 대기 상태가 된다.
운영자 스레드가 배송 상태로 변경한 뒤 트랜잭션을 커밋하면 잠금을 해제한다.
잠금이 해제된 시점에 고객 스레드가 구하는 주문 애그리거트는 운영자 스레드가 수정한 배송 상태의 주문 애그리거트다.
배송 상태이므로 주문 애그리거트는 배송지 변경 시 에러를 발생하고 트랜잭션은 실패하게 된다. 이 시점에 고객은 ‘이미 배송이 시작되어 배송지를 변경할 수 없습니다.’와 같은 문구를 보게 된다.
: 선점 잠금은 보통 DBMS가 제공하는 행단위 잠금을 사용해서 구현한다.
: 오라클을 비롯한 다수의 DBMS가 for update와 같은 쿼리를 사용해서 특정 레코드에 한 커넥션만 접근할 수 있는 잠금장치를 제공한다.
: JPA EntityManager는 LockModeType을 인자로 받는 find() 메서드를 제공한다. LockModeType.PERSSIMISTIC_WRITE를 값으로 전달하면 해당 엔티티와 매핑된 테이블을 이용해서 선점 잠금 방식을 적용할 수 있다.
Order order = entityManager.find(Order.class, orderNo, LockModeType.PESSIMISTIC_WRITE);
Java
복사
: JPA 프로바이더와 DBMS에 따라 잠금 모드 구현이 다르다. 하이버네이트의 경우 PESSIMISTIC_WRITE를 잠금 모드로 사용하면 ‘for update’ 쿼리를 이용해서 선점 잠금을 구현한다.
: 스프링 데이터 JPA는 @Lock 애너테이션을 사용해서 잠금 모드를 구현한다.
public interface MemberRepository extends Repository<Member, MemberId> { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select m from Member m where m.id = :id") Optional<Member> findByIdForUpdate(@Param("id") MemberId memberId); }
TypeScript
복사

1. 선점 잠금과 교착 상태

: 선점 잠금 기능을 사용할 때는 잠금 순서에 따른 교착 상태가 발생하지 않도록 주의해야 한다. 예를 들어 다음과 같은 순서로 두 스레드가 잠금 시도를 한다고 해보면
1.
스레드1 : A 애그리거트에 대한 선점 잠금 구함
2.
스레드2 : B 애그리거트에 대한 선점 잠금 구함
3.
스레드1 : B 애그리거트에 대한 선점 잠금 시도
4.
스레드2 : A 애그리거트에 대한 선점 잠금 시도
: 이 순서에 따르면 스레드1은 영원히 B 애그리거트에 대한 선점 잠금을 구할 수 없다.
: 스레드2가 B 애그리거트에 대한 잠금을 이미 선점하고 있기 때문이다. 동일한 이유로 스레드2는 A 애그리거트에 대한 잠금을 구할 수 없다.
: 두 스레드는 상대방 스레드가 먼저 선점함 잠금을 구할 수 없어 더 이상 다음 단계를 진행하지 못하게 되고, 이는 곧 교착 상태로 이어진다.
: 선점 잠금에 따른 교착 상태는 일반적으로 사용자 수가 많을 때 발생할 가능성이 높다.
: 사용자 수가 많아지면 교착 상태에 빠지는 스레드는 더 빠르게 증가한다. 더 많은 스레드가 교착 상태에 빠질수록 시스템은 아무것도 할 수 없는 상태가 된다.
: 이런 문제가 발생하지 않도록 하려면 잠금을 구할 때 최대 대기 시간을 지정해야 한다. JPA에서는 선점 잠금을 시도할 때 최대 대기 시간을 지정하려면 다음과 같이 사용한다.
Map<String, Object> hints = new HashMap<>(); hints.put("javax.persistence.lock.timeout", 2000); Order order = entityManager.find(Order.class, orderNo, LockModeType.PESSIMISTIC_WRITE, hints);
TypeScript
복사
: JPA의 ‘javax.persistence.lock.timeout’ 힌트는 잠금을 구하는 대기 시간을 밀리초 단위로 지정한다. 지정한 시간 이내에 잠금을 구하지 못하면 익셉션을 발생시킨다.
: 이 힌트를 사용할 때 주의할 점은 DBMS에 따라 힌트가 적용되지 않을 수도 있다는 것이다. 해당 기능을 지원하는지 확인해야 한다.
: 스프링 데이터 JPA는 @QueryHints 애너테이션을 사용해서 쿼리 힌트를 지정할 수 있다.
public interface MemberRepository extends Repository<Member, MemberId> { @Lock(LockModeType.PESSIMISTICRITE) @QueryHints({ @QueryHint(name = "javax.persistence.lock.timeout", value = "2000") }) @Query("select m from Member m where m.id = :id") Optional<Member> findByIdForUpdate(@Param("id") MemberId memberId);
TypeScript
복사