Chapter 8: 애그리거트 트랜잭션 관리
💡 책에서 기억하고 싶은 내용
애그리거트와 트랜잭션
한 애그리거트를 두 사용자가 동시에 변경할 때
트랜잭션
이 필요하다애그리거트에 대해 사용할 수 있는 대표적인 트랜잭션 처리 방식에는
선점(Pessimistic) 잠금
과비선점(Optimistic) 잠금
두 가지 방식이 있다
선점 잠금
먼저 애그리거트를 구한 스레드가
애그리거트 사용이 끝날 때까지
다른 스레드가 해당 애그리거트를 수정하지 못하게 막는 방식잠금된 동안
블로킹
된다
한 스레드가 애그리거트를 구하고 수정하는 동안 다른 스레드가 수정할 수 없으므로, 동시에 애그리거트를 수정할 때 발생하는
데이터 충돌 문제
를 해소할 수 있다선점 잠금을 이용해서
트랜잭션 충돌 문제
를 해결한다
선점 잠금은 보통 DMBS가 제공하는 행단위 잠금을 이용해서 구현한다
JPA Provider와 DBMS에 따라 잠금 모드 구현이 다른데,
스프링 데이터 JPA
는@Lock
annotation을 사용해서 잠금 모드를 지정한다
선점 잠금과 교착 상태
선점 잠금을 사용할 때는 잠금 순서에 따른
교착 상태
가 발생하지 않도록 주의해야 한다교착 상태 발생을 막기 위해 잠금을 구할 때
최대 대기 시간
을 지정해야 한다스프링 데이터 JPA
는@QueryHints
annotation을 사용해서쿼리 힌트
를 지정할 수 있다
비선점 잠금
선점 잠금으로 모든 트랜잭션 충돌 문제가 해결되지는 않는다
이때 필요한 것이
비선점 잠금
이다
비선점 잠금 은 동시에 접근하는 것을 막는 대신, 변경한 데이터를
DBMS에 반영하는 시점
에변경 가능여부를 확인
하는 방식이다비선점 잠금을 구현하려면 애그리거트에
버전
으로 사용할숫자 타입 프로퍼티
를 추가해야 한다애그리거트를 수정할 때마다 버전으로 사용할 프로퍼티 값이 1씩 증가하는데, 이때 다음과 같은 쿼리를 사용한다
이 쿼리는
수정할 애그리거트
와 매핑되는테이블의 버전 값
이 현재 애그리거트의 버전과동일한 경우
에만 데이터를 수정한다수정에 성공하면 버전 값을 1 증가시킨다
다른 트랜잭션이 먼저 데이터를 수정해서 버전 값이 바뀌면, 데이터 수정에 실패하게 된다
JPA는
버전
을 이용한 비선점 잠금 기능을 지원한다버전으로 사용할 필드에
@Version
annotation을 붙이고, 매핑되는 테이블에 버전을 저장할 칼럼을 추가한다ex)
JPA는 엔티티가 변경되어 UPDATE 쿼리를 실행할 때,
@Version
에 명시한 필드를 이용해서 비선점 잠금 쿼리를 실행한다응용 서비스는 버전에 대해 알 필요가 없다!
리포지터리에서 필요한 애그리거트를 구하고, 알맞은 기능만 실행하면 된다
기능 실행 과정에서 애그리거트가 변경되면, JPA는 트랜잭션 종료 시점에 비선점 잠금을 위한 쿼리를 실행한다
비선점 잠금을 위한 쿼리를 실행할 때, 실행 결과로 수정된 행의 개수가 0이면, 이미 누군가 앞서 데이터를 수정한 것이다
이는 트랜잭션이
충돌
한 것이므로, 트랜잭션 종료 시점에Exception
이 발생한다트랜잭션 충돌이 발생하면
OptimisticLockingFailureException
이 발생한다표현 영역
코드는 이Exception
이 발생했는지에 따라트랜잭션 충돌
이 일어났는지확인
할 수 있다
응용 서비스에서 버전 일치 여부를 확인하는 method를 두고, 일치하지 않으면 버전이 충돌했다는
Exception
을 발생시켜표현 계층
에 이를 알린다VersionConflictException
표현 계층은 버전 충돌 Exception이 발생하면
버전 충돌
을 사용자에게 알려, 사용자가 알맞은후속 처리
를 할 수 있도록 한다비선점 잠금과 관련해서 발생하는 두 개의 Exception
하나는 스프링 프레임워크가 발생시키는
OptimisticLockingFailureException
이고,다른 하나는 응용 서비스에서 발생시키는
VersionConflictException
이다
이 두 Exception은 개발자 입장에서는
트랜잭션 충돌이 발생한 시점
을 명확하게 구분해 준다VersionConflictException
은 이미 누군가가 애그리거트를 수정했다는 것을 의미하고,OptimisticLockingFailureException
은 누군가가 거의 동시에 애그리거트를 수정했다는 것을 의미한다
버전 충돌 상황에 대한 구분이 명시적으로 필요 없다면, 응용 서비스에서 프레임워크용 Exception을 발생시키는 것도 고려할 수 있다
강제 버전 증가
애그리거트에 애그리거트 루트 외에 다른 엔티티가 존재하는데 기능 실행 도중 루트가 아닌 다른 엔티티의 값만 변경될 경우, JPA는
루트 앤티티
의버전 값
을증가시키지 않는다
연관된 엔티티의 값이 변경된다고 해도 루트 앤티티 자체의 값은 바뀌는 것이 없으므로, 루트 엔티티의 버전 값은 갱신하지 않는 것이다
but, 이런 JPA 특징은
애그리거트 관점
에서 보면 문제가 된다루트 엔티티의 값이 바뀌지 않았더라도, 애그리거트의 구성요소 중 일부 값이 바뀌면 논리적으로 그 애그리거트는 바뀐 것이다
따라서 애그리거트 내에
어떤 구성요소의 상태가 바뀌면
루트 애그리거트의버전 값이 증가
해야 비선점 잠금이 올바르게 동작한다JPA는 이런 문제를 처리할 수 있도록
EntityManager#find()
method로 엔티티를 구할 대 강제로 버전 값을 증가시키는잠금 모드
를 지원한다LockModeType.OPTIMISTIC_FORCE_INCREMENT
를 사용하면 해당 엔티티의 상태가 변경되었는지에 상관없이트랜잭션 종료 시점
에 버전 값증가
처리를 한다이 잠금 모드를 사용하면 애그리거트 루트 앤티티가 아닌 다른 앤티티나 밸류가 변경되더라도 버전 값을 증가시킬 수 있으므로 비선점 잠금 기능을 안전하게 적용할 수 있다
스프링 데이터 JPA를 사용하면
@Lock
annotation을 이용해서 지정하면 된다
오프라인 선전 잠금
오프라인 선전 잠금은 단일 트랜잭션에서 동시 변경을 막는 선점 잠금 방식과 달리,
여러 트랜잭션
에 걸쳐동시 변경
을 막는다첫 번째 트랜잭션을 시작할 때
오프라인 잠금
을선점
하고, 마지막 트랜잭션에서 잠금을해제
한다잠금을 해제하기 전까지 다른 사용자는 잠금을 구할 수 없다!
오프라인 선점 방식은
잠금 유효 시간
을 가져야 한다유효 시간이 지나면 자동으로 잠금을 해제해서 다른 사용자가 잠금을 일정 시간 후에 다시 구할 수 있도록 해야한다
ex)
수정 폼에서 1분 단위로 Ajax 호출을 해서 잠금 유효 시간을 1분씩 증가시키기
오프라인 선점 잠금을 위한 LockManager 인터페이스와 관련 클래스
오프라인 선점 잠금은 네 가지 기능이 필요하다
잠금 선점 시도
잠금 확인
잠금 해제
잠금 유효시간 연장
위 기능을 위한
LockManager
인터페이스를 사용할 수 있다잠금을 선점한 이후에 실행하는 기능은 아래의 상황을 고려하여 반드시 주어진
LockId
를 갖는 잠금이 유효한지 확인해야 한다잠금 유효 시간이 지났으면 이미 다른 사용자가 잠금을 선점한다
잠금을 선점하지 않은 사용자가 기능을 실행했다면, 기능 실행을 막아야 한다
Last updated