비선점 잠금은 버전을 통해 변경 가능 여부를 확인한다.
선점 잠금으로 모든 트랜잭션 충돌 문제가 해결되는 것은 아니다.
운영자가 배송지 정보를 조회하고 배송 상태로 변경하는 사이 고객이 배송지를 변경한 상황이다.
이처럼 동시에 접근하는 것을 막는 대신 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 비선점 잠금이 필요하다.
애그리거트 버전으로 사용할 숫자 타입 프로퍼티를 추가해야 한다.
UPDATE aggtable SET version = version + 1, colx = ?, coly = ? WHERE aggid = ? and version = 현재 버젼
수정할 애그리거트와 현재 애그리거트의 버전이 동일한 경우에만 쿼리가 수행된다.
다음과 같이 수정에 성공하면 버전을 1 증가시키고, 버전 값이 다르면 수정에 실패한다.
JPA는 @Version 어노테이션을 사용해 비선점 잠금 기능을 구현할 수 있다.
스프링의 @Transactional 어노테이션을 사용해 트랜잭션이 종료될 때 충돌이 발생하면 OptimisticLockingFailureException을 발생시킨다.
처음 충돌 문제가 발생했던 상황에 비선점 잠금을 적용하면 다음과 같은 흐름으로 이루어지게 된다.
2.1.2는 버전 A와 B가 다르기 때문에 수정할 수 없는 상황이고 2.1.3은 버전 A와 B가 같아 배송 상태를 변경한 상황이다.
@Controller
public class OrderAdminController {
private StartShippingService startShippingService;
@RequestMapping(value = "/startShipping", method = RequestMethod.POST)
public String startShipping(StartShippingRequest startReq) {
try {
startShippingService.startShipping(startReq);
return "shippingStarted";
} catch(OptimisticLockingFailureException | VersionConflicException ex) {
// 트랜잭션 충돌
return "startShippingTxConflict";
}
}
...
다음 코드는 스프링 프레임워크가 발생시키는 OptimisticLockingFailureException과 응용 서비스에서 발생시키는 VersionConflicException을 처리하고 있다.
VersionConflicException은 이미 누군가가 애그리거트를 수정했다는 것을 의미하고, OptimisticLockingFailureException은 누군가가 거의 동시에 수정했다는 것을 의미한다.
JPA는 애그리거트 루트가 아닌 다른 엔티티가 변경되었을 때 루트 엔티티 자체의 값은 바뀌지 않으므로 버전 값을 갱신하지 않는다.
하지만 애그리거트 관점에서 보았을 때 애그리거트의 구성요소가 바뀌면 논리적으로 애그리거트도 바뀐 것이다.
JPA는 이런 문제를 처리하기 위해 조회 쿼리를 수행할 때 LockModeType.OPTIMISTIC_FORCE_INCREMENT를 사용해 트랜잭션 종료 시점에 버전 값 증가 처리를 한다.
비선점 방식은 선점 방식과 다르게 행단위로 잠금을 하는 것이 아닌 버전을 통해 변경 가능 여부를 확인한다.
그렇기 때문에 교착 상태가 발생할 위험이 적고 안전하다.
하지만 매번 버전을 비교해야 하는 것과 사용자 측에서도 버전을 알아야 하는 것이 단점이다.
선점 방식이 필요한 경우도 있고 비선점 방식이 필요한 경우도 있으니 알맞는 상황에 사용하면 좋을 것 같다.
트랜잭션 처리 기법과 관련해서 공부를 해 상황을 판단하는 힘을 길러야 한다.