본문 바로가기

programming/DDD

[DDD] chapter 8

반응형

chapter 8

8.1 애그리거트와 트랜잭션

한 주문 애그리거트에 대해 운영자는 배송 상태로 변경할 때 사용자는 배송지 주소를 변경하면 다음과 같은 일이 일어난다.

 

운영자 스레드 고객 스레드
주문 애그리거트 구함  
  주문 애그리거트 구함
배송 상태로 변경  
  배송지 변경
트랜잭션 커밋  
  트랜잭션 커밋

 

트랜잭션마다 리포지터리는 새로운 애그리거트 객체를 생성하므로 운영자 스레드와 고객 스레드는 같은 주문 애그리거트를 나타내는 다른 객체를 구하게 된다.

 

운영자 스레드와 고객 스레드는 개념적으로 동일한 애그리거트지만 물리적으로 서로 다른 애그리거트 객체를 사용한다. 때문에 운영자 스레드가 주문 애그리거트 객체를 배송 상태로 변경 하더라도 고객 스레드가 사용하는 주문 애그리거트 객체에는 영향을 주지 않는다. 고객 스레드 입장에서 주문 에그리거트 객체는 아직 배송 상태 전이므로 배송지 정보를 변경할 수 있다.

 

이 상황에서 두 스레드는 각각 트랜잭션을 커밋할 때 수정한 내용을 DB에 반영한다. 이 시점에 배송 상태로 바뀌고 배송지 정보도 바뀌게 된다. 이 순서의 문제점은 운영자는 기존 배송지 정보를 이용해서 배송 상태로 변경했는데 그 사이 고객은 배송지 정보를 변경했다는 점이다. 즉, 애그리거트의 일관성이 깨지는 것이다.

 

일관성이 깨지는 문제가 발생하지 않도록 하려면 다음 두가지 중 하나를 해야한다.


  • 운영자가 배송지 정보를 조회하고 상태를 변경하는 동안, 고객이 애그리거트를 수정하지 못하게 막는다.
  • 운영자가 배송지 정보를 조회한 이후에 고객이 정보를 변경하면, 운영자가 애그리거트를 다시 조회한 뒤 수정하도록 한다.

이 두가지는 애그리거트 자체의 트랜잭션과 관련이 있다. DBMS가 지원하는 트랜잭션과 함께 애그리거트를 위한 추가적인 트랜잭션 처리 기법이 필요하다. 애그리거트에 대해 사용할 수 있는 대표적인 트랜잭션 처리 방식에는 선점 잠금과 비선점 잠금의 두 가지 방식이 있다.

8.2 선점 잠금

선점 잠금은 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드가 해당 애그리거트를 수정하지 못하게 막는 방식이다.

 

스레드 1 스레드 2
애그리거트 구함 (접근 잠금) 애그리거트 구함 시도 (잠금으로 대기)
애그리거트 수정 잠금된 동안 블로킹
트랜잭션 커밋 (잠금 해제) 애그리거트 구함(접근 잠금)
  애그리거트 수정
  트랜잭션 커밋 (잠금 해제)

 

위의 표에서 스레드1이 선점 잠금 방식으로 애그리거트를 구한 뒤 이어서 스레드2가 같은 애그리거트를 구하고 있다. 이때 스레드2는 스레드1이 애그리거트에 대한 잠금을 해제할 때까지 블로킹 된다.

 

한 스레드가 애그리거틀 구하고 수정하는 동안 다른 스레드가 수정할 수 없으므로 동시에 애그리거트를 수정할 때 발생하는 데이터 충돌 문제를 해소할 수 있다.

 

선점 잠금은 보통 DBMS가 제공하는 행단위 잠금을 사용해서 구현한다.

 

JPA Entity Manager는 LockModeType을 인자로 받는 find() 메서드를 제공한다. LockModeType.PESSIMISTIC_WRITE를 값으로 전달하면 해당 엔티티와 매핑된 테이블을 이요해서 선점 잠금 방식을 적용할 수 있다.

 

8.2.1 선점 잠금과 교착 상태

선점 잠금 기능을 사용할 때는 잠금 순서에 따른 교착 상태가 발생하지 않도록 해야한다.


1. 스레드1: A 애그리거트에 대한 선점 잠금 구함

2. 스레드2: B 애그리거트에 대한 선점 잠금 구함

3. 스레드1: B애그리거트에 대한 선점 잠금 시도

4. 스레드2: A 애그리거트에 대한 선점 잠금 시도


위의 순서에 따르면 스레드 1은 영원히 B애그리거트에 대한 선점 잠금을 구할 수 없다.

 

선점 잠금에 따른 교착 상태는 상대적으로 사용자 수가 많을 때 발생할 가능성이 높고, 사용자 수가 많아지면 교착 상태에 빠지는 스레드는 더 빠르게 증가한다. 더 많은 스레드가 교착 상태에 빠질수록 시스템은 아무것도 할 수 없는 상태가 된다.

 

이런 문제가 발생하지 않도록 잠금을 구할 때 최대 대기 시간을 지정해야 한다.

 

Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.lock.timeout", 2000);
Order order = entityManager.find(Order.class, orderNo, LockModeType.PESSIMISIC_WRITE, hints);

 

JPA의 javax.persistence.lock.timeout 힌트는 잠금을 구하는 대기 시간을 밀리초 단위로 지정한다. 지정한 시간 이내에 잠금을 구하지 못하면 익셉션을 발생시킨다.

 

8.3 비선점 잠금

위의 예시에서  운영자가 배송지 정보를 조회하고 배송 상태로 변경하는 그  순간에 고객이 배송지를 변경하면 오류가 발생할 수 있다. 즉, 배송 상태 변경 전에 배송지를 한 번 더 확인하지 않으면 운영자는 다른 배송지로 물건을 발송하게 된다.

 

이 문제는 비선점 잠금으로 해결할 수 있다.비선점 잠금은 동시에 점근하는 것을 막는 대신 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식이다.

 

스레드1 스레드2
애그리거트 구함 (버전 1)  
  애그리거트 구함(버전 1)
애그리거트 수정  
  애그리거트 수정
트랜잭션 커밋 -> 버전 1에서 2로 변경  
  트랜잭션 커밋 실패 버전 1을 커밋할 때 버전을 비교

 

(천재인가...?)

 

연산이 많이 느는 것 같지도 않아 좋은 방법 같다.

 

JPA는 비선점 잠금 기능을 지원한다. 다음과 같이 버전으로 사용할 필드에 @Version 애너테이션을 붙이고 매핑되는  테이블에 버전을 저장할 수있다.

 

@Entity
@Table
public class Order {
    @EmbeddedId
    private OrderNo number;
    
    @Version
    private long version;
}

 

JPA Entity가 변경되어 Update 쿼리를 실행할 때 @Version에 명시한 필드를 사용해 잠금 쿼리를 실행한다.

8.4 오프라인 선점 잠금

오프라인 서점 잠금은 여러 트랜잭션에 걸쳐 동시 변경을 막는다. 첫 번째 트랜잭션을 시작할 때 오프라인 잠금을 선점하고 마지막 트랜잭션에서 잠금을 해제한다.

8.4.1 오프라인 선점 잠금을 위한 LockManager 인터페이스와 관련 클래스



 

반응형

'programming > DDD' 카테고리의 다른 글

[DDD] chapter 10  (1) 2024.03.09
[DDD] chapter 9  (1) 2024.02.27
[DDD] chapter 7  (0) 2024.02.16
[DDD] chapter 6  (0) 2024.02.10
[DDD] chapter 5  (1) 2024.02.07