8장 애그리거트 트랜잭션 관리
8.1 애그리거트와 트랜잭션
DBMS가 지원하는 트랜잭션과 함께 애그리거트를 위한 추가적인 기법이 필요하다. 대표적인 트랜잭션 처리 방식에는 선점 잠금과 비선점 잠금 방식이 있는데 이를 살펴보자.
8.2 선점 잠금
선점 잠금은 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드가 해당 애그리거트를 수정하지 못하게 막는 방식이다. 스레드1이 애그리거트를 구한 뒤 이어서 스레드2가 같은 애그리거트를 구하고 있다. 스레드 2는 스레드1이 애그리거트에 대한 잠금을 해제할 때까지 블로킹된다.
한 스레드가 애그리거트를 구하고 수정하는 동안 다른 스레드가 수정할 수 없으므로 동시에 애그리거트를 수정할 때 발생하는 데이터 충돌 문제를 해소할 수 있다. 선점 잠금은 보통 DBMS가 제공하는 행단위 잠금을 사용해서 구현한다. 오라클을 비롯한 다수의 DBMS가 for update와 같은 쿼리를 사용해서 특정 레코드에 한 커넥션만 접근할 수 있다.
선점 잠금 기능을 사용할 때 잠금 순서에 따른 교착 상태가 발생하지 않도록 주의해야 한다. 상대방 스레드가 먼저 선점한 잠금을 구할 수 없어 다음 단계를 진행하지 못하게 된다. 선점 잠금에 따른 교착 상태는 상대적으로 사용자 수가 많을 때 발생할 가능성이 높고, 교착 상태에 빠지는 스레드는 빠르게 증가한다. 이를 위해 최대 대기 시간을 지정해야 한다. JPA에서 선점 잠금을 시도할 때 최대 대기 시간을 지정하려면 힌트를 사용한다.
8.3 비선점 잠금
하지만 선점 잠금으로 해결할 수 없는 상황이 있다. 운영자가 배송지 정보를 조회하고 배송 상태로 변경하는 사이에 고객이 배송지를 변경할 때이다. 배송 상태 변경 전에 배송지를 한번 더 확인하지 않으면 서로 교차하는 상황이 발생한다. 이때 필요한 것이 비선점이다. 비선점 잠금은 동시에 접근하는 것을 막는 대신 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식이다.
이를 구현하려면 애그리거트에 버전으로 사용할 숫자 타입 프로퍼티를 추가해야 한다. 애그리거트를 수정할 때마다 버전으로 사용할 프로퍼티 값이 1씩 증가한다. 아래 쿼리를 예로, 수정할 애그리거트와 매핑되는 테이블의 버전 값이 현재 애그리거트의 버전과 동일한 경우에만 데이터를 수정한다. 수정에 성공하면 버전 값을 1 증가시킨다. 다른 트랜잭션이 먼저 데이터를 수정해서 버전 값이 바뀌면 데이터 수정에 실패한다.
JPA는 버전을 이용한 비선점 잠금 기능을 지원한다. @Version 애너테이션을 붙이고 매핑되는 테이블에 버전을 저장할 칼럼을 추가하면 된다. JPA는 엔티티가 변경되어 UPDATE 쿼리를 실행할 때 @Version에 명시한 필드를 이용해서 비선점 잠금 쿼리를 실행한다.
예로 시스템은 사용자에게 수정폼을 제공할 때 애그리거트 버전을 함께 제공하고, 폼을 서버에 전송할 때 이 버전을 함께 전송한다. 사용자가 전송한 버전과 애그리거트 버전이 동일한 경우에만 애그리거트 수정 기능을 수행하도록 함으로써 트랜잭션 충돌 문제를 해소할 수 있다.
만약 충돌 익셉션이 발생하면 버전 충돌을 사용자에게 알려 후속 처리를 할 수 있도록 한다. VersionConflictException은 이미 누군가가 애그리거트를 수정했다는 것을 의미하고, OptimisticLockingFailureException은 누군가가 거의 동시에 애그리거트를 수정했다는 것을 의미한다.
8.3.1 강제 버전 증가
애그리거트 내 어떤 구성요소의 상태가 바뀌면 루트 애그리거트의 버전 값이 증가해야 비선점 잠금이 올바르게 동작한다. 이를 위해 JPA는 EntityManager#find() 메서드로 엔티티를 구할 때 강제로 버전 값을 증가시키는 잠금 모드를 지원한다. LockModeType.OPTIMISTIC_FORCE_INCREMENT를 사용하면 엔티티의 상태가 변경되는지 상관없이 트랜잭션 종료 시점에 버전 값 증가 처리를 한다. 이는 루트 엔티티가 아닌 다른 엔티티나 밸류가 변경되더라도 버전 값을 증가시킬 수 있기에 비선점 잠금 기능을 적용할 수 있다.
8.4 오프라인 선점 잠금
한 트랜잭션 범위에서만 적용되는 선점 잠금 방식이나 나중에 버전 충돌을 확인하는 비선점 잠금 방식으로 해결할 수 없는 문제가 있는데 이때 오프라인 선점 잠금 방식이 사용된다. 단일 트랜잭션에서 동시 변경을 막는 선점 잠금 방식과 달리 오프라인 선점 점금은 여러 트랜잭션에 걸쳐 동시 변경을 막는다.
예로 수정 기능을 보자. 2개의 트랜잭션으로 구성되고, 첫 번째 트랜잭션은 폼을 보여주고, 두 번째 트랜잭션은 데이터를 수정한다. 오프라인 선점 잠금을 사용하면 폼 요청 과정에서 잠금을 선점하고, 수정 과정에서 잠금을 해제한다. 이미 잠금을 선점한 상태에서 다른 사용자가 폼을 요청하면 잠금을 구할 수 없어 에러 화면을 보게 된다. 또한 잠금 유효 시간을 가져야 한다.