1. 애그리거트와 트랜잭션
위와 같이 두 사용자가 거의 동시에 한 애그리거트를 변경하고자할 때 데이터 레이스(Data Race)가 발생하여 애그리거트의 일관성이 깨지게 되는데, 이러한 문제가 발생하지 않도록 하기 위해선 트랜잭션 처리가 필요하다.
대표적인 트랜잭션 처리 방식에는 선점(Pessimistic) 잠금과 비선점(Optimistic) 잠금이 있다.
2. 선점 잠금
선점 잠금은 애그리거트를 먼저 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드가 해당 애그리거트를 수정하는 것을 막는 방식이다.
선점 잠금과 교착 상태
선점 잠금을 사용할 때는 잠금 순서에 따른 교착 상태(Deadlock)가 발생하지 않도록 주의해야 한다.
교착 상태란 서로 다른 애그리거트에 대해 두 쓰레드가 각각의 애그리거트를 선점 잠금을 해놓은 상태에서 서로의 애그리거트를 선점 잠금하려고 시도하는 경우에 계속해서 기다리고 있는 상태를 의미한다. 이러한 교착 상태는 상대적으로 사용자 수가 많을 때 발생할 가능성이 높고, 교착 상태에 빠지는 스레드가 더 빠르게 증가하게 된다.
이러한 문제가 발생하지 않도록 하기 위해선 잠금을 구할 때 최대 대기 시간을 지장함으로써 해결할 수 있다.
DBMS에 따라 교착 상태에 빠진 커넥션을 처리하는 방법이 다르기 때문에 사용하는 DBMS에 대해 JPA가 어떤 식으로 처리하는지 확인해야 한다.
3. 비선점 잠금
아래와 같은 상황처럼 선점 잠금으로 모든 트랜잭션 충돌 문제가 해결되는 것은 아니다.
여기서 문제는 운영자는 고객이 변경하기 전의 배송지 정보를 이용해서 배송 준비를 한 뒤에 배송 상태로 변경하게 된다. 즉, 배송 상태 변경 전에 배송지를 한 번 더 확인하지 않으면 운영자는 다른 배송지로 물건을 발송하게 된다.
이는 비선점방식으로 해결할 수 있는데, 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식이다. 애그리거트에 버전(Version) 프로퍼티를 추가하여 애그리거트 수정 시도시 현재 애그리거트의 버전과 일치하는지 확인한 뒤, 성공시 버전 값을 1씩 증가하는 방식이다.
// 응용 서비스는 버전에 대해 알 필요가 없다. 레포지터리에서 피룡한 애그리거트를 구하고 알맞은 기능만 실행하면 된다.
public class ChangeShippingService {
// chnageShipping() 메서드가 리턴될 때 트랜잭션이 종료되고,
// 이 시점에 트랜잭션 충돌이 발생하면 OptimisticLockingFailureException을 발생시킨다.
@Transactional
public void changeShipping(ChangeShippingRequest changeReq) {
Order order = orderRepository.findById(new OrderNo(changeReq.getNumber()));
checkNoOrder(order);
order.changeShippingInfo(changeReq.getShippingInfo());
}
}
강제 버전 증가
애그리거트엔 애그리거트 루트 외에 다른 엔티티가 존재하는데, 기능 실행 도중 루트가 아닌 다른 엔티티의 값만 변경되는 경우가 있다. 이 경우 일반적으로 JPA는 루트 엔티티의 버전 값을 증가하지 않는다.
그런데, 이런 JPA 특징은 애그리거트 관점에서 보면 문제가 된다. 실제 루트 엔티티의 값이 바뀌진 않았지만, 애그리거트의 구성요소 중 일부 값이 바뀌어 논리적으로는 그 애그리거트는 바뀐 것이기 때문이다. 따라서, 이 경우에도 루트 애그리거트의 버전 값을 증가해야 비선점 잠금이 올바르게 동작한다.
JPA의 경우엔 이런 문제를 처리할 수 있도록 EntityManager#find() 메서드로 엔티티를 구할 때 강제로 버전 값을 증가시키는 잠금 모드를 지원하고 있다. (여기서 JPA의 상세 기능에 대해선 다루지 않겠다.)
4. 오프라인 선점 잠금
지라(Jira)로 유명한 아틀라시안의 컨플루언스 위키는 문서를 편집하려고 할 때, 누군가 먼저 편집 중이라면 다른 사용자가 문서를 수정하고 있다는 안내 문구를 보여준다. 이런 안내를 통해 여러 사용자가 동시에 한 문서를 수정할 때 발생하는 충돌을 사전에 방지할 수 있지만, 그렇다고 강제로 막고 있지는 않다. 이러한 방식을 구현하기 위해서 필요한 것이 오프라인 선점 잠금 방식(Offline Pessimistic Lock)이다.
단일 트랜잭션에서 동시 변경을 막는 선점 잠금과 달리 오프라인 선점 잠금은 여러 트랜잭션에 걸쳐 동시 변경을 막는다.