애그리거트와 트랜잭션
한 주문 애그리거트에 대해 운영자는 뱅송 상태로 변경할 때 사용자는 배송지 주소를 변경할 때를 생각해보자.
한 애그리거트를 두 사용자가 동시에 변경할 때 트랜잭션이 필요하다.
트랜잭션마다 리포지터리는 새로운 애그리거트 객체를 생성하므로 운영자 스레드와 고객 스레드는 같은 주문 애그리거트를 나타내는 다른 객체를 구하게 된다.
때문에 운영자 스레드가 주문 애그리거트 객체를 배송 상태로 변경하더라도 고객 스레드가 사용하는 주문 애그리거트 객체에는 영향을 주지 않는다.
고객 스레드 입장에서 주문 애그리거트 객체는 아직 배송 상태 전이므로 배송지 정보를 변경할 수 있다.
이 상황에서 두 스레드는 가각 트랜잭션을 커밋할 때 수정한 내용을 DB에 반영한다.
이 시점에 배송 상태로 바뀌고 배송지 정보도 바뀌게 된다. 이 순서의 문제점은 운영자는 기존 배송지 정보를 이용해서 배송 상태로 변경했는데 그 사이 고객은 배송지 정보를 변경했다는 점이다.
즉, 애그리거트의 일관성이 깨지는 것이다.
일관성이 깨지는 문제가 발생하지 않도록 하려면 다음 두 가지 중 하나를 해야 한다.
- 운영자가 배송지 정보를 조회하고 상태를 변경하는 동안, 고객이 애그리거트를 수정하지 못하게 막는다.
- 운영자가 배송지 정보를 조회한 이후에 고객이 정보를 변경하면, 운영자가 애그리거트를 다시 조회한 뒤 수정하도록 한다.
이 두 가지는 애그리거트 자체의 트랜잭션과 관련이 있다.
DBMS가 지원하는 트랜잭션과 함께 애그리거트를 위한 추가적인 트랜잭션 처리 기법이 필요하다.
애그리거트에 대해 사용할 수 있는 대표적인 트랜잭션 처리 방식에는 선점 잠금과 비선점 잠금의 두 가지 방식이 있다.
선점 잠금
- Pessimistic Lock
- 비관적 잠금
비선점 잠금
- Optimistic Lock
- 낙관적 잠금
선점 잠금
- 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드가 해당 애그리거트를 수정하지 못하게 막는 방식
스레드1의 커밋이 끝날 때 까지 스레드2는 블로킹이 되고 커밋이 될 때 잠금이 해제된다.
스레드2가 애그리거트에 접근하게 되는데 스레드1이 트랜잭션을 커밋한 뒤에 스레드2가 애그리거트를 구하게 되므로 스레드2는 스레드1이 수정한 애그리거트의 내용을 보게 된다.
한 스레드가 애그리거트를 구하고 수정하는 동안 다른 스레드가 수정할 수 없으므로 동시에 애그리거트를 수정할 때 발생하는 데이터 충돌 문제를 해소할 수 있다.
JPA의 애노테이션으로 선점 잠금을 구현할 수 있다.
@Lock(LockModeType……)
선점 잠금과 교착 상태
선점 잠금 기능을 사용할 때는 잠금 순서에 따른 교착 상태가 발생하지 않도록 주의해야 한다.
예를 들어, 다음과 같은 순서로 두 스레드가 잠금 시도를 한다고 해보자.
- 스레드1 : A 애그리거트에 대한 선점 잠금 구함
- 스레드2 : B 애그리거트에 대한 선점 잠금 구함
- 스레드1 : B 애그리거트에 대한 선점 잠금 시도
- 스레드2 : A 애그리거트에 대한 선점 잠금 시도
이 순서에 따르면 스레드1은 영원히 B 애그리거트에 대한 선점 잠금을 구할 수 없다.
왜냐하면 스레드2가 B 애그리거트에 대한 잠금을 이미 선점하고 있기 때문이다. 동일한 이유로 스레드2는 A 애그리거트에 대한 잠금을 구할 수 없다.
두 스레드는 상대방 스레드가 먼저 선점한 잠금을 구할 수 없어 더 이상 다음 단계를 진행하지 못하게 된다. 즉, 스레드1과 스레드2는 교착 상태에 빠진다.
선점 잠금에 따른 교착 상태는 상대적으로 사용자 수가 많을 때 발생할 가능성이 높고, 사용자 수가 많아지면 교착 상태에 빠지는 스레드는 더 빠르게 증가한다.
더 많은 스레드가 교착 상태에 빠질수록 시스템은 아무것도 할 수 없는 상태가 된다.
이러한 문제가 발생하지 않도록 하려면 잠금을 구할 때 최대 대기 시간을 지정해야한다.
JPA에서는 @QueryHint({name = “javax.persistence.lock.timeout”, value = “2000”}) 을 설정한다.
메모.
각 DBMS 마다 교착 상태에 빠진 커넥션을 처리하는 방식이 다르다.
쿼리별로 대기 시간을 지정할 수 있는 것이 있고 커넥션 단위로만 대기 시간을 지정할 수 있는 것이 있다.
선점 잠금을 사용하려면 사용하는 DBMS에 대해 JPA가 어떤 식으로 대기 시간을 처리하는지 반드시 확인해야 한다.
비선점 잠금
- 동시에 접근하는 것을 막는 대신 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식
- 변경 가능 여부
- version을 이용
그림으로 이해하기
JPA에서 설정 방법
버전으로 사용할 필드에 @Version 애너테이션을 붙이고 매핑되는 테이블에 버전을 저장할 칼럼을 추가하면 된다.
- 비선점 잠금 방식을 여러 트랜잭션으로 확장하려면 애그리거트 정보를 뷰로 보여줄 때 버전 정보도 함계 사용자 화면에 전단해야 한다.
- 예를 들어, 배송 상태 변경을 처리하는 응용 서비스가 전달받는 데이터는 다음과 같이 주문 번호와 함계 해당 주문을 조회한 시점의 버전 값을 포함해야 한다.
- 응용 서비스는 전달받은 버전 값을 이용해서 애그리거트의 버전과 일치하는지 확인하고 일치하는 경우에만 요청한 기능을 수행한다.
- 표현 계층은 버전 충돌 익셉션이 발생하면 버전 충돌을 사용자에게 알려주고 사용자가 알맞은 후속 처리를 할 수 있도록 한다.
강제 버전 증가
- 애그리거트에 애그리거트 루트 외에 다른 엔티티가 존재하는데 기능 실행 도중 루트가 아닌 다른 엔티티의 값만 변경된다고 하자.
- 이 경우 JPA는 루트 엔티티의 버전 값을 증가시키지 않는다.
- 연관된 엔티티의 값이 변경된다고 해도 루트 엔티티 자체의 값은 바뀌는 것이 없으므로 루트 엔티티의 버전 값은 갱신하지 않는 것이다.
- 그런데 이런 JPA 특징은 애그리거트 관점에서 보면 문제가 된다.
- 비록 루트 엔티티의 값이 바뀌지 않았더라도 애그리거트의 구성요소 중 일부 값이 바뀌면 논리적으로 그 애그리거트는 바뀐 것이다.
- 따라서 애그리거트 내에 어떤 구성요소의 상태가 바뀌면 루트 애그리거트의 버전 값이 증가해야 비선점 잠금이 올바르게 동작한다.
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
- 해당 엔티티의 상태가 변경되었는지에 상관없이 트랜잭션 종료 시점에 버전 값 증가 처리를 한다.
- 이 잠금 모드를 사용하면 애그리거트 루트 엔티티가 아닌 다른 엔티티나 밸류가 변경되더라도 버전 값을 증가 시킬 수 있으므로 비선점 잠금 기능을 안전하게 적용할 수 있다.
'Book > 도메인 주도 계발 시작하기' 카테고리의 다른 글
도메인 서비스 (0) | 2022.12.28 |
---|---|
스프링 데이터 JPA를 이용한 조회 기능 (0) | 2022.12.26 |
응용 서비스와 표현 영역 (0) | 2022.12.26 |
리포지터리와 모델 구현 (0) | 2022.12.22 |
애그리거트 (0) | 2022.12.21 |
아키텍처 개요 (0) | 2022.12.21 |
도메인 모델 시작하기 (0) | 2022.12.20 |