애그리거트의 동시성 문제와 트랜잭션 관리
주문 애그리거트의 루트 엔티티인 Order 클래스가 존재한다고 가정한다. 또, 주문 애그리거트는 주문 상태가 배송 상태가 아닌 경우에만 배송지를 변경할 수 있다는 정책이 존재한다. 만약 하나의 주문 애그리거트에 대해 관리자 스레드가 주문 상태를 배송으로 변경함과 동시에 고객 스레드가 배송지를 변경한다면 문제가 발생할 것이다.
예를 들면 위와 같이, 관리자가 주문 레코드를 읽고(1), 그 다음 고객이 주문 레코드를 읽는다(2). 해당 상태에서는관리자와 고객 모두 본인이 원하는 변경(주문승인 → 배송중, 경기도 → 강원도)이 가능한 상태이다. 여기서 관리자가 먼저 배송승인 상태를 배송중 상태로 변경(3)하고 커밋했다면? 해당 애플리케이션의 주문 정책에 따르면 고객은 배송지를 변경할 수 없다. 하지만 동시성 문제를 잘 고려하지 않았다면, 고객은 주문을 읽는 시점에 배송지 변경이 가능하다고 판단하여 실제로는 배송중 상태인 주문 정보의 배송지를 변경(4)해버릴 것이다. 결과적으로 애그리거트의 일관성이 깨지게 된다.
해당 문제는 관리자와 고객 스레드가 각각 논리적으로는 동일한 애그리거트(레코드)를 읽고 사용하지만 물리적으로는 서로 다른 애그리거트를 사용함으로 발생한다. 즉, 관리자 스레드에서의 애그리거트 변경 사항은 고객 스레드에 영향을 끼치지 않고 그 반대도 마찬가지이다. 해당 문제를 해결하기 위해서는 공유자원에 대한 동시접근의 문제를 해결하는 방안을 생각해야 한다. 데이터베이스 레벨에서는 비관적 락을 고려할 수 있을 것이고, 애플리케이션 레벨에서는 낙관적 락 혹은 synchronized
를 사용하여 동일한 락을 공유하는 방식(비효율적, 서버 인스턴스가 여러 대인 경우 DB레벨에서 동시성 문제가 발생할 수 있음)을 고려할 수 있을 것이다.
비관적 락 사용 시 운영자가 주문 정보를 조회하고 상태를 변경하는 동안, 고객은 애그리거트를 수정하지 못하도록 한다. 낙과적 락 사용 시 운영자가 주문 정보를 조회한 이후 애그리거트 변경이 발생했다면(version 정보를 통해 확인한다.), 애그리거트를 다시 조회한 후 작업을 수행한다.
비관적 락 사용
Spring Data JPA 환경에서 비관적 락 사용하기
@Repository
public interface ProductRepository extends JpaRepository<ProductEntity, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE) // 쓰기(배타)락을 건다.
Optional<ProductEntity> findByDescription(String description);
@Lock(LockModeType.PESSIMISTIC_READ) // 읽기(공유)락을 건다.
Optional<ProductEntity> findByName(String description);
}
비관적 락을 사용하는 경우 데드락을 항상 주의해야 한다. 데드락 감지 알고리즘에 의해 데드락을 해결하거나, 타임아웃을 두어 일정 시간동안 락을 획득하지 못한다면 트랜잭션을 종료하는 방식으로 데드락을 예방할 수 있다.
비관적 락의 경우 성능이 좋지 않아질 우려가 존재한다. 예를 들어 한 트랜잭션이 select ~ for update 쿼리를 통해 exclusive Lock을 획득하려 했으나 해당 레코드 혹은 인덱스에 shared Lock 이 걸려있는 상태라 대기중이다. 다른 트랜잭션이 shared Lock을 반환하기 전 또 다른 트랜잭션이 shared Lock 을 획득해버리고, 해당 현상이 계속해서 반복되다가 결국 update 를 위한 트랜잭션이 작업을 수행하지 못하는 현상이 발생할 수 있다.
낙관적 락 사용
낙관적 락을 사용하기 위해서는 애그리거트와 레코드에 버전으로 사용할 숫자 프로퍼티 값을 추가해야 한다. 애그리거트가 수정될 때마다 버전을 1씩 증가하여 스레드가 애그리거트를 읽었을 때의 버전과 수정할 때의 버전을 체크하여 애그리거트를 수정하기 전 다른 스레드에 의해 애그리거트가 수정되었는지 확인하는 방식이다.
UPDATE aggtable SET version=version+1, colx=?, coly=?
WHERE aggid=? and version={read 했을 때의 버전};
위의 쿼리는 애그리거트를 업데이트할 때 버전이 읽었을 때의 버전과 동일하지 않다면 변경을 실패한다.
Spring Data JPA에서는 version 정보를 아래처럼 구현할 수 있다.
@Getter
@Entity
public class PersonEntity {
...
@Version
private Long version;
...
}
버전 정보가 일치하지 않아 업데이트를 수행하지 못했다면 OptimisticLockingFailureException이 발생한다. 보통 비관적락을 사용하여 동시성을 처리하는 방식을 사용하는 경우는 흔치 않고, 낙과적 락을 사용하여 동시성 문제를 대부분 해결할 수 있다고 한다.
Reference
도메인주도 개발 시작하기:DDD 핵심 개념 정리부터 구현까지(최범균, 2022)
'CS' 카테고리의 다른 글
CQRS (0) | 2024.04.07 |
---|---|
DDD - 외부 시스템 호출 분리와 이벤트 (0) | 2024.04.06 |
DDD - 도메인과 애그리거트 (1) | 2024.03.27 |
Blocking vs Non-Blocking I/O (0) | 2024.03.26 |
IPC: 메시지 포맷과 설계 순서 (3) | 2024.03.19 |