DDD Start라는 책을 읽다가, 웹 애플리케이션에서 여러 스레드가 특정 모델(애그리거트)에 동시 접근할 때 발생하는 문제와 해결법이 나오는데 실무에서 꼭 알아두어야할 내용인 것 같아서 정리하려고 한다.
온라인 쇼핑몰 어플리케이션에서 고객과 운영자가 주문 정보를 수정하는 상황을 가정해보자.
운영자가 배송을 수행하고 배송상태를 "배송"으로 변경하면 고객은 배송지를 변경할 수 없어야 한다. 프로그램 상에 배송지 변경 불가에 대한 비즈니스 룰이 있더라도, 위와 같은 상황에서는 이미 고객이 보고있는 주문 정보는 배송 상태가 아직 "배송 전" 이기 때문에 두 트랜잭션이 모두 처리가 되면 문제가 발생한다.
이 문제를 해결하는 방법으로 먼저 선점 잠금을 살펴보자.
선점 잠금은 주문 정보(애그리거트)를 구한 스레드가, 해당 주문 정보의 사용이 끝날 때까지 다른 스레드가 해당 주문 정보를 수정하는 것을 막는 방식이다.
4번째 과정에서, 잠금이 해제되고 나서 고객 스레드가 구한 데이터는 배송상태가 변경되어 있기때문에 배송지 변경을 실패처리할 수 있게 된다.
선점 잠금은 이런 상황에서 굉장히 유용한 방법이긴 하나 종종 교착 상태를 유발할 수 있다.
또 모든 트랜잭션 충돌 문제를 해결해주진 않는다.
다음의 상황을 살펴보자.
배송 상태 변경 전에 운영자가 배송지를 한번 더 확인하지 않으면 다른 배송지로 물건을 발송하게 된다.
선점 잠금에서는 이 문제를 해결할 수 없다. 배송 상태가 변경되기 전까지 배송지가 변경되는 것은 시스템 상 허용되어야 하기 때문이다.
이 문제를 해결하기 위해 비선점 잠금을 활용할 수 있다.
비선점 잠금은 변경한 데이터를 실제 데이터베이스에 반영하는 시점에 변경 가능 여부를 확인하는 방식이다. 이를 하기 위해서 모델(애그리거트)에 버전으로 사용할 숫자 타입의 attribute를 추가해야 한다. 모델을 수정할 때마다 버전으로 사용하는 attribute의 값이 1씩 증가하며, 다음과 같은 쿼리를 통해 업데이트를 수행한다.
UPDATE model SET version = version + 1, colx = ? WHERE model.id = ? and version = 현재 버전
위 쿼리는 데이터베이스에 저장된 모델의 버전 값이 현재 내가 보고있는 모델의 버전과 동일할 경우에만 데이터를 수정한다. 또 수정할 때 버전 값을 1을 증가시킨다. 만약 다른 스레드에서 트랜잭션이 성공하여 버전 값이 바뀌면 이전 버전 값을 들고 있는 트랜잭션에서 업데이트는 실패할 것이다.
만약에 더 엄격하게 데이터 충돌을 예방하고 싶다면 누군가 수정 화면을 보고 있을 경우 수정 화면 자체를 실행하지 못하도록 막을 수 있다. 기존의 선점 잠금 방식은 한 트랜잭션 범위에만 적용되어 이를 구현할 수 없고, 비선점잠금 방식은 추후 버전 충돌을 확인하기 때문에 다른 방법이 필요하다. 이 때 사용할 수 있는 방법이 오프라인 선점 잠금이다.
운영자1이 주문 정보를 수정하려고 페이지에 접근하여 Lock을 획득한다면, 타 운영자가 주문을 수정하기 위해 접근하면 Lock 상태를 보여주며 에러 페이지를 반환한다.
따라서 여러 트랜잭션에 거쳐 동시 변경을 막을 수 있다. 다만, Lock을 점유하고 있는 쪽이 아무런 행동을 하지 않을 수도 있기 때문에 Lock 유효시간을 적절히 설정해야 한다.
웹 애플리케이션에서 동시성 문제는 아주 중요하다. 비즈니스 요구사항을 잘 분석하고, 발생할 수 있는 문제들을 잘 고민하여 적절한 방법을 도입해야겠다.