Aggregate의 Transaction 관리

gentledot·2021년 12월 5일
0
post-thumbnail
  • 한 주문 애그리거트에 대해 운영자는 배송 상태로 변경할 때 사용자는 배송지 주소를 변경하면?
    그림 8.1 한 애그리거트를 두 사용자가 동시에 변경할 때 트랜잭션이 필요하다.
    • 메모리 캐시를 사용하지 않을 경우 운영자 스레드와 고객 스레드는 같은 주문 애그리거트를 나타내는 다른 객체를 생성한다.
    • 운영자 스레드와 고객 스레드는 개념적으로 동일한 애그리거트이지만 물리적으로 서로 다른 애그리거트 객체를 시용한다.
    • 때문에 운영자 스레드가 주문 애그리거트 객체를 배송 상태로 변경하더라도 고객 스레드가 사용하는 주문 애그리거트 객체 에는 영향을 주지 않는다. 고객 스레드 입장에서 주문 애그리거트 객체는 아직 배송 상대 전이므로 배송지 정보를 변경할 수 있다.
    • 이 상황에서 두 스레드는 기존 배송지 정보를 이용해서 배송 상태로 변경할 수 있고, 그 사이 고객은 배송지 정보를 변경할 수 있기 때문에 애그리거트의 일관성이 깨질 수 있는 것이다.
  • 애그리거트의 일관성을 보장하러면 다음의 두 가지 중 하나를 해야 한다.
    • 운영자가 배송지 정보를 조회하고 상태를 변경하는 동안 고객이 애그리거트를 수정하지 못하게 막는다.
    • 운영자가 배송지 정보를 조회한 이후에 고객이 정보를 변경하면 운영자가 애그리거트를 다시 조회한 뒤 수정하도록 한다.
  • 이 두 가지는 애그리거트 자체의 트렌잭션과 관련이 있다. DBMS가 지원하는 트랜잭션과 함께 애그리거트를 위한 추가적인 트랜잭션 처리 기법이 필요하다.
    • Pessimistic(선점) Lock(잠금) (비관적 잠금)
    • Optimistic(비선점) Lock(잠금) (낙관적 잠금)

Pessimistic Lock

  • 선점 잠금(Pessimistic Lock)은 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드가 해당 에그리거트를 수정하는 것을 막는 방식이다.
    그림 8.2 선점 잠금의 동작 방식
    • 스레드1이 선점 잠금 방식으로 애그리거트를 구한 경우에 스레드2는 애그리거트에 대한 잠금을 해제할 때까지 블로킹된다.
    • 스레드1이 애그리거트를 수정하고 트랜잭션을 커밋하면 잠금을 해제한다. 이 순간 대기하고 있던 스레드2가 애그리거트에 접근하게 된다.
    • 스레드2가 애그리거트를 구할 때는 스레드1이 수정한 애그리거트의 내용을 보게 된다.
    • 한 스레드가 애그리거트를 구하고 수정하는 동안 다른 스레드가 수정할 수 없으므로 동시에 애그리거트를 수정할 때 발생하는 데이터 충돌 문제를 해소할 수 있다.
  • 운영자 스레드가 먼저 선점 잠금 방식으로 주문 애그리거트를 구한 경우 운영자 스레드가 잠금을 해제할 때까지 고객 스레드는 대기 상태가 되고 잠금이 해제된 시점에 고객 스레드가 구하는 주문 애그리거트는 운영자 스레드가 수정한 배송 상태의 주문 애그리거트이다.
    • 배송 상태이므로 주문 애그리거트는 배송지 변경 시 에러를 발생하고 트랜잭션은 실패하게 된다. ("이미 배송이 시작되어 배송지 를 변경할 수 없습니다" 와 같은 안내 문구를 호출)
  • 선점 잠금은 보통 DBMS가 제공하는 행 단위 잠금을 사용해서 구현한다.
    • 오라클을 비롯한 다수 DBMS가 for update 와 같은 쿼리를 사용해서 특정 레코드에 한 사용자만 접근할 수 있는 잠금 장치를 제공한다.
  • JPA의 EntityManager는 LockModeType을 인자로 받는 find() 메서드를 제공한다.
    public abstract <T> T find(   
    	Class<T> aClass,
      Object o,
      javax.persistence.LockModeType lockModeType 
    )
    // Maven: org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final (hibernate-jpa-2.1-api-1.0.0.Final.jar)
    • LockModeType.PESSIMISTIC_WRITE 값으로 전달하면 해당 엔티티와 매핑된 테이블을 이용해서 선점 잠금 방식을 적용할 수 있다.
    • LockModeType
      javax.persistence.LockModeType 타입
  • JPA 프로바이더와 DBMS에 따라 잠금 모드의 구현이 다른데, 하이버네이트의 경우PESSIMISTIC_WRITE 잠금 모드로 사용하면 for update 쿼리를 사용해시 선점 잠금을 구현한다.

선점 잠금과 교착 상태

  • 선점 잠금 기능을 사용할 때는 잠금 순서에 따른 교착 상태(Deadlock)가 발생하지 않도록 주의해야 한다.
    • 예를 들어, 다음과 같은 순서로 두 스레드가 잠금 시도를 한다면
      1. 스레드1: A 애그리거트에 대한 선점 잠금 구함
       2. 스레드2: B 애그리거트에 대한 선점 잠금 구함
       3. 스레드1: B 애그리거트에 대한 선점 잠금 시도
       4. 스레드2: A 애그리거트에 대한 선점 잠금 시도
    • 이 순서에 따르면 스레드1은 영원히 B 애그리거트에 대한 선점 잠금을 구할 수 없다. ⇒ 스레드2가 B 애그리거트에 대한 잠금을 이미 선점하고 있기 때문이다.
    • 동일한 이유로 스레드2는 A 애그리거트에 대한 잠금을 구할 수 없다. ⇒ 스레드2가 A 애그리거트에 대한 잠금을 이미 선점하였음.
    • 두 스레드는 상대방 스레드가 먼저 선점한 잠금을 구할 수 없어 더 이상 디음 단계를 진행하지 못하며 스레드1과 스레드2는 교착 상태에 빠지게 되는 것이다.
  • 선점 잠금에 따른 교착 상태는 상대적으로 사용자 수가 많을 때 발생할 가능성이 높고, 사용자 수가 많아지면 교착 상태 에 빠지는 스레드가 더 빠르게 증가하게 된다.
    • 더 많은 스레드가 교착 상태에 빠질수록 시스템은 점점 아무것도 할 수 없는 상황에 이르게 된다.
  • 이런 문제가 발생하지 않도록 하려면 잠금을 구할 때 최대 대기 시간을 지정해야 한 다. JPA에서 선점 잠금을 시도할 때 최대 대기 시간을 지정하려면 다음과 같이 힌트를 사용하면 된다
    public abstract <T> T find(   
    	Class<T> aClass,
      Object o,
      javax.persistence.LockModeType lockModeType,
      java.util.Map<String, Object> map 
    )
    // Maven: org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final (hibernate-jpa-2.1-api-1.0.0.Final.jar)
  • JPA의 javax.persistence.lock.timeout 힌트는 잠금을 구하는 대기 시간을 밀리초 단위로 지정한다. 지정한 시간 이내에 잠금을 구하지 못하면 익셉션을 발생시킨다.
    Map<String, Object> hints = new HashMap();
    hints.put("javax.persistence.lock.timeout", 2000);
    
    Order order = entityManager.find(
        Order.class, orderNo, 
        LockModeType.PESSIMISTIC_WRITE, hints);
    • 힌트를 사용할 때 주의할 점은 DBMS에 따라 힌트가 적용되지 않는다는 것이다. 힌트를 이용할 때에는 사용 중인 DBMS가 관련 기능을 지원하는지 확인해야 한다. (DBMS에 대해 JPA가 어떤 식으로 대기 시간을 처리하는지 반드시 확인)
    • DBMS에 따라 교착 상태에 빠진 커넥션을
      • 쿼리별로 대기 시간을 지정할 수 있고
      • 커넥션 단위로만 대기 시간을 지정할 수 있는 DBMS도 있음.
profile
그동안 마신 커피와 개발 지식, 경험을 기록하는 공간

0개의 댓글