[DDD] 도메인 주도 개발 시작하기 - 8장

Y_Sevin·2023년 8월 26일
0

8.1 애그리거트와 트랜잭션

한 주문 애그리거트에 대해 운영자는 배송 준비 상태로 변경할 때 사용자는 배송지 주소를 변경한다면 어떻게 될까

  • 스레드는 각각 트랜잭션을 커밋할 때 수정한 내용을 DBMS에 반영함.
  • 때문에 하나의 애그리거트를 여러 사용자가 동시에 변경하고자한다면 데이터의 일관성이 깨질 수 있음

이런 문제를 방지하기 위해서는 아래와 같은 방법을 사용해야 함

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

위와 같이 일관성이 깨지지도 않도록 하는 방법은 트랜잭션 처리와 관련이 있음
애그리거트에 대해 사용할 수 있는 대표적인 트랜잭션 처리 방식에는 의 두 가지 방식이 존재함

  • 선점(Pessimistic) 잠금
  • 비선점(Optimistic) 잠금

8.2 선점 잠금

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

한 스레드가 애그리거트를 구하고 수정하는 동안 다른 스레드가 수정할 수 없도록 하여 일관성을 지킬 수 있음

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

  • 일반적으로 DBMS 에서는 for update 와 같은 쿼리를 사용해서
    특정 레코드에 한 사용자만 접근할 수 있는 잠금 장치를 제공함
  • JPA 의 EntityManager 에서는 LockModeType.PESSIMISTIC_WRITE 사용
  • 스프링 데이터 JPA에서는 @Lock(LockModeType.PESSIMISTIC_WRITE) 어노테이션 사용

8.2.1 선전 잠금과 교착 상태

선점 잠금 기능을 사용할 때는 잠금 순서에 주의하여 교착 상태(dealock)가 발생하지 않도록 해야 함

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

예를 들어, 위 같은 순서로 두 스레드가 잠금 시도를 한다면 스레드2가 B 애그리거트에 대한 잠금을 이미 선점하고 있기 때문에 스레드1은 영원히 B 애그리거트에 선점 잠금을 구할 수 없음

이러한 문제를 방지하기 위해, 선점 잠금을 시도하는 최대 대기 시간을 지정한다.

//JPA 에서는 아래와 같이 최대 대기 시간을 걸 수 있음
Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.lock.timeout", 2000);
Order order = entityManager.find( Order.class, orderNo, 
		LockModeType.PRESSIMISTIC_WRITE, hints);
        

8.3 비선점 잠금

선점 잠금을 사용한다고 해서 모든 트랜잭션 충돌 문제가 해결되는 것은 아님

  1. 운영자는 배송을 위해 주문 정보를 조회한다. 시스템은 정보를 제공한다.
  2. 고객이 배송지 변경을 위해 변경 폼을 요청한다. 시스템은 변경 폼을 제공한다.
  3. 고객이 새로운 배송지를 입력하고 폼을 전송해서 배송지를 변경한다.
  4. 운영자가 1번에서 조회한 주문 정보를 기준으로 배송지를 정하고 배송 상태 변경을 요청한다.

위와 같이 1번에서 조회한 주소를 4번 배송 상태 변경에 사용하게 된다면 변경 이전 주소로 배송이 될 수 있음

이러한 문제는 비선점 잠금을 활용하여 해결 할 수 있음

비선점 잠금은 애그리거트에 버전을 추가하여 수정할 애그리거트와 매핑되는 테이블의 버전 값이 현재 애그리거트의 버전과 동일한 경우에만 데이터를 수정함.

//JPA에서의 비선점 잠금
@Entity
@Table(name = "purchage_order")
@Access(AccessType.FIELD)
public class Order {
	@EmbeddedId
	private OrderNo number;

	@Version // 매핑되는 테이블에 버전을 저장
	private long version;
	
	...
}

JPA는 엔티티가 변경되어 UPDATE 쿼리를 실행할 때 @Version에 명시한 필드를 이용해서 비선점 잠금 쿼리를 실행 함

// 버전이 일치하는 경우에만 수정
UPDATE purchage_order SET ..., version = version + 1
	WHERE number = ? and version = 10 
@Controller
public class OrderController {
	...
	@RequestMapping(value = "/changeShipping", method = RequestMethod.POST)
	public String changeShipping(ChangeShippingRequest changeReq) {
		try {
			changeShippingService.changeShipping(changeReq);
			return "changeShippingSuccess";
		} catch(optimisticLockingFailureException ex) {
				// 누군가 같은 주문 애그리거트를 수정했다면 트랜잭션 충돌이 일어났다는 메시지를 보여줌
				return "changeShippingExConflic";
		}
}

강제 버전 증가
루트 엔티티와 연관된 엔티티의 값이 변경될 경우 루트 엔티티 자체의 값은 바뀌는 것이 없으므로 버전 값을 갱신하지 않음
때문에 버전 값을 강제로 증가시켜야 비선점 잠금이 올바르게 동작함

  • JPA에서는 EntityManager.find() 메서드로 강제로 버전 값을 증가시키는 잠금 모드를 지원함

8.4 오프라인 선점 잠금

데이터 충돌을 엄격하게 막기 위해 누군가 수정화면을 보고 있다면 다른 사람은 수정화면을 실행하지 못하게 할 수 있음
한 트랜잭션 범위에서만 적용되는 선점 잠금 방식이나 나중에 버전 충돌을 확인하는 비선점 잠금 방식으로는 이를 구현할 수 없기 때문에 오프라인 선점 잠금 방식을 사용함

오프라인 선점 잠금은 여러 트랜잭션에 걸쳐 동시 변경을 막는 방식이며 잠금을 해제하기 전까지 다른 사용자는 잠금을 구할 수 없음


위와 같이 오프라인 선점 잠금 방식을 사용하지만 사용자 A 가 3의 수정 요청을 수행하지 않고 프로그램을 종료하면 다른 사용자는 영원히 잠금을 구할 수 없는 상황이 발생함

  • 오프라인 선점 잠금 방식은 유효시간을 가져야하며 유효시간이 자니면 자동으로 잠금이 해제되도록 해야함
  • 또한 접근해 있는 사용자는 일정 주기로 유효시간 연장 api 를 호출하도록 하여 수정 중에 잠금이 해제되는 일을 방지
profile
매일은 아니더라도 꾸준히 올리자는 마음으로 시작하는 개발블로그😎

0개의 댓글