[DDD START] 8장 애그리거트 트랜잭션 관리 - 02. 낙관적 잠금

낙관적 잠금(Optimistic, 비선점)

변경한 데이터를 실제 데이터베이스에 반영하는 시점에 변경 가능 여부를 확인하는 방식

애그리거트에 버전(숫자 타입)을 추가하고 애그리거트가 수정될 때마다 버전을 1씩 증가시킨다.

낙관적 잠금의 작동 방식

  1. 스레드 1과 스레드 2는 버전 5의 애그리거트를 읽어온다.
  2. 스레드 1이 수정에 성공하고 버전이 6이 된다.
  3. 스레드 2가 커밋을 시도하는데 애그리거트의 버전이 6이므로 처음에 읽어온 버전과 달라서 수정에 실패한다.

JPA에서 적용

Entity에 @Version을 추가한다.

@Entity
public class Order {
    @Id
    private Long id;

    private String orderNumber;

    @Version
    private long version;
}

응용 서비스는 버전에 대해 알 필요없이 알맞은 기능만 실행하면 된다.

public class ChangeShippingService {

    @Transactional
    public void changeShipping(ChangeShippingRequest changeShippingRequest) {
        Order order = orderRepository.findByOrderNumber(changeShippingRequest.getOrderNumber());
        order.changeShippingInfo(changeShippingRequest.getShippingInfo());
    }
}

트랜잭션이 충돌하면 OptimisticLockingFailureException이 발생하고 표현 영역에서 확인할 수 있다.

@Controller
public class OrderController {
    private ChangeShippingService changeShippingService;

    @PostMapping(value = "/changeShipping")
    public String ChangeShipping(ChangeShippingRequest changeShippingRequest) {
        try {
            changeShippingService.changeShipping(changeShippingRequest);
            return "changeShippingSuccess";
        } catch (OptimisticLockingFailureException e) {
            return "changeShippingTxConflict";
        }
    }
}

낙관적 잠금을 이용한 트랜잭션 충돌 방지를 여러 트랜잭션으로 확장

  1. 주문 데이터를 요청할 때 버전 값을 받는다(1번 과정).
  2. 배송 상태를 변경할 때 버전 값도 함께 전송한다(2번 과정).
  3. 과정 1과 과정 2.1.1에서 받은 버전이 다르면 과정 1과 과정 2사이에 다른 사용자가 해당 애그리거트를 수정한 것이다.
  4. 이 경우 2.1.2와 같이 수정할 수 없다는 에러를 응답으로 전송한다.
  5. 버전 A와 버전 B가 같다면 누구도 애그리거트를 수정하지 않은 것이다.
  6. 이 경우 2.1.3과 같이 애그리거트를 수정하고 변경 내용을 DBMS에 반영한다.
  7. 2.1.1과 2.1.4 사이에 누군가 애그리거트를 수정해서 커밋했다면 버전값이 증가한 상태가 되므로 트랜잭션 커밋은 실패한다.

그림과 같이 낙관적 잠금을 여러 트랜잭션으로 확장하려면 애그리거트 정보를 뷰로 보여줄 때 버전 정보도 함께 사용자 화면에 전달해야 한다. 애그리거트의 수정을 요청할 때 버전 정보를 함께 보낸다.

public class StartShippingRequest {

    private String orderNumber;
    private long version;
}

응용 서비스는 전달받은 버전 값을 이용해서 애그리거트의 버전과 일치하는지 확인한다.

public class StartShippingService {

    @PreAuthorize("hasRole('ADMIN')")
    @Transactional
    public void startShipping(StartShippingRequest startShippingRequest) {
        Order order = orderRepository.findByOrderNumber(startShippingRequest.getOrderNumber());
        checkOrder(order);
        if (!order.matchVersion(startShippingRequeset.getVersion())) {
            throw new OptimisticLockingFailureException("version conflict");
        }
        order.startShipping();
    }
}

강제 버전 증가

애그리거트에 애그리거트 루트 외에 다른 엔티티가 존재하는데 기능 실행 도중 루트가 아닌 다른 엔티티의 값만 변경된다고 하자. 이 경우 JPA는 루트 엔티티의 버전 값을 증가시키지 않는다. 루트 엔티티 자체의 값은 바뀌는 것이 없으므로 루트 엔티티의 버전 값을 갱신하지 않는 것이다.

루트 엔티티의 값이 바뀌지 않았더라도 애그리거트의 구성요소 중이 일부가 바뀌면 논리적으로 애그리거트는 바뀐 것이다. 따라서, 애그리거트 내에 어떤 구성요소의 상태가 바뀌면 루트 애그리거트의 버전 값을 증가해야 낙관점 잠금이 올바르게 동작한다.

이런 문제를 처리할 수 있도록 JPA는 EntityManager#find() 메서드로 엔티티를 구할 때 버전 값을 증가시키는 잠금 모드를 지원하고 있다.

@Repository
public class orderRepository {
    
    @PersistenceContext
    private EntityManager entityManager;

    public Order findByIdOptimisticLockMode(Long id) {
        return entityManager.find(
            Order.class, id, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
    }
}

Reference