: 선점 잠금은 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드들이 해당 애그리거트를 수정하지 못하도록 막는 방식
•
스레드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.PESSIMISTIC少 RITE)
@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
복사