From 5682175b2c86e7b05158ef2285be58d09d5b3385 Mon Sep 17 00:00:00 2001 From: jaeyeon kim Date: Wed, 31 Jul 2024 19:20:30 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20chap08=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\352\271\200\354\236\254\354\227\260.md" | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 "chap08/\352\271\200\354\236\254\354\227\260.md" diff --git "a/chap08/\352\271\200\354\236\254\354\227\260.md" "b/chap08/\352\271\200\354\236\254\354\227\260.md" new file mode 100644 index 0000000..7aab3dd --- /dev/null +++ "b/chap08/\352\271\200\354\236\254\354\227\260.md" @@ -0,0 +1,173 @@ +# 애그리거트 트랜잭션 관리 + +## 애그리거트와 트랜잭션 + +![](https://i.imgur.com/xRfD0eP.png) + +만일, 주문 애그리거트에 대해 운영자는 배송 상태로 변경, 사용자는 배송지 주소를 변경하면 위와 같이 되게 될 것이다. + +이렇게 됨으로써, 배송 상태로 변경이 되면서, 배송지 변경까지 되는 등의 일관성이 맞지 않는 상황으로 가게 될 것이다. + +이러한 상황을 막기 위해서 두 가지를 고려해야한다. + +- 운영자가 배송지 정보를 조회하고 상태를 변경하는 동안, 고객이 애그리거트를 수정하지 못하게 막는다. +- 운영자가 배송지 정보를 조회한 이후에 고객이 정보를 변경하면, 운영자가 애그리거트를 다시 조회한 뒤 수정하도록 한다. + +사실 트랜잭션의 개념을 알고 있다면, 이러한 문제를 트랜잭션으로 해결할 수 있다는 것을 인지할 수 있다. + +## 선점 잠금 + +선점 잠금은 곧 `비관적 락`이라고 표현한다. + +![](https://i.imgur.com/xNFu42l.png) + +다음과 같이 굉장히 무식하지만 확실한 방법이다. + +스레드 1이 트랜잭션을 커밋하게 되면 잠금을 해제하고 이 순간, 대기하고 있던 스레드 2가 애그리거트에 접근하게 된다. + +> 운영자가 배송지 정보를 조회하고 상태를 변경하는 동안, 고객이 애그리거트를 수정하지 못하게 막는다. + +이렇게 됨으로써 이전의 1번째 문제가 해결이 된다. + +비관적 락은 DBMS가 제공하는 행단위 잠금을 사용해서 구현하게 된다. (for update와 같은 쿼리를 사용해서, innoDB같은 경우, 해당 쿼리가 언두로그에 접근하는 것을 막아서, Phantom Read가 Repeatable Read까지도 발생하게 된다.) + +Spring Data JPA에서 이를 사용하는 방법은 다음과 같다. + +![](https://i.imgur.com/hUUIqXy.png) + +### 선점 잠금과 교착 상태 + +비관적 락을 사용하게 되는 경우, 교착상태가 발생할 수 있다. + +다음과 같은 상황에서 말이다. + +![](https://i.imgur.com/aUR7ZyV.png) +이 경우 절대로 스레드1은 절대로 B 애그리거트의 락을 얻을 수 없다. + +이미 스레드 2가 B를 선점 중이고, 스레드 2는 A를 선점할 때까지, 본인이 가진 락을 놓지 않으면 되니 말이다. + +하지만, 작업을 모두 마쳤을 때, 락을 해제하면 되지 않을까? + +그래서, 최대 대기 시간을 설정 해놓을 수 있다. + +최대 대기 시간을 설정하기 위해서는, 힌트를 적어놓아야 한다. + +Spring Data JPA 에서는, 힌트를 QueryHints로 적용할 수 있다. + +![](https://i.imgur.com/skkxsgG.png) + +간단하게, 적용하기에는 또, 비관적 락만한 것이 없는 것 같긴하다. + +## 비선점 잠금 + +![](https://i.imgur.com/gBlvCIk.png) + +비관적 락으로 해결할 수 없는 경우이다. + +비관적 잠금은 실제로 변경을 진행할 때, 락을 걸어버리는 것이라고 볼 수 있다. + +하지만, 이는 조회를 진행하고, 실제로 변경을 실행할 때, 다른 트랜잭션에서 변경 작업을 진행하고 난 뒤이니 비관적 락으로는 해결할 수 없는 것이다. + +이를 막기 위해서는 내가 변경하려고 할 때, 내가 조회한 버전과, 현재의 버전이 동일한 버전인지 확인하면 되는 것인데, 이것이 낙관적 락이라고 보면 된다. + +![](https://i.imgur.com/lCR1HII.png) + +이를 구현하게 되면, DB딴에서는 데이터가 변경될 때마다, version을 1씩 올려주게 된다. + +![](https://i.imgur.com/hxXktwf.png) + +그림으로 표현하게 되면 이렇게 된다. + +Spring Data JPA 에서 이를 구현하는 방법은 또 아주 쉽게 + +![](https://i.imgur.com/lx9pAJ4.png) + +@Version 컬럼을 만들어주면된다. + +이는 응용 서비스에서 신경쓸 필요가 전혀 없다. + +알아서, JPA가 해주게 되고, 만일 충돌이 나게 되면 OptimisticLockingFailureException 이 발생하게 된다. (하나의 트랜잭션에서 조회하고 저장하는 경우) + +![](https://i.imgur.com/mNHcMWR.png) + +정리하면 위와 같이 되게 된다. + +위처럼 하나의 트랜잭션이 아닌 길게 가져가는 경우, 이 Version 값을 응용서비스 로직으로 가져갈 수 있다. + +![](https://i.imgur.com/0CyGXyA.png) + +그래서, 여기서 명확하게 얻어갈 수 있는 점도 있는 것이, VersionConflictException이 발생한 경우는, 누군가가 여러 트랜잭션에 걸쳐, 과정을 진행했을 때, Conflict가 난 것을 의미하고, OptimisticLockingFailureException 은 하나의 트랜잭션에서 충돌이 발생했음을 의미한다. + +### 강제 버전 증가 + +루트 애그리거트가 아닌, 그 내부에 있는 엔티티가 변경이 되었다면, 루트 애그리거트 엔티티의 Version값은 동일할 것이다. 하지만, 개념적으로 이도 애그리거트가 변경된 것으로 인지를 해야하기에, 강제 버전 증가를 활용해서, 트랜잭션 종료 시점에 무조건 버전 값 증가 처리를 하면 된다. + +그러면 실제로 변경하지 않았더라도, Version이 올라가게 될 것이니까, 하지만 충돌이 더욱 많이 발생하지 않을까? + +## 오프라인 선점 잠금 + +![](https://i.imgur.com/q3D1hwQ.png) + +이 개념은 조금 재미있는 것 같다. + +이번에는 하나의 트랜잭션이 아닌 상태에서도 다른 트랜잭션으로부터의 접근을 막기 위해 오프라인 잠금을 구하는 것을 설명하고 있다. + +오프라인 잠금을 얻게 되면 하나의 트랜잭션이 조회를 진행하고 있을 때, 다른 트랜잭션이 접근을 하지 못한다. + +또한, 오프라인 잠금 해제는 다른 트랜잭션에서 하기에, 굉장히 긴 잠금 상태를 가질 수도 있다. + +그렇기에, 유효 시간이 지나면 자동으로 잠금을 해제하여, 다른 사용자가 잠금을 일정 시간 후에 다시 구할 수 있도록 해야한다. + +효과적인 방법으로는 유효 시간을 짧게 가져가는 것이 좋아보인다. 하지만, 유저가 오랫동안 수정하고 있을 수도 있지 않을까? 그러면 유저는 오래 수정했다는 이유만으로 수정에 실패하게 될 것이다. 이를 방지하기 위해 서버에 마치 refresh token처럼 유효시간을 늘릴 수 있는 요청도 보내줄 수 있다. + +### 오프라인 선점 잠금을 위한 LockManager 인터페이스와 관련 클래스 + +![](https://i.imgur.com/6Bk49Gy.png) + +오프라인 선점은 잠금 선점 시도, 잠금 확인, 해제, 유효시간 연장등의 기능이 필요하다. + +tryLock은 type과 id를 파라미터로 갖는데, 잠글 대상 타입과 식별자를 값으로 전달하면 된다. (ex) 식별자가 10인 Article에 대한 잠금을 구하고 싶으면, type을 domain.Article, id를 10으로 주면된다.) + +당연히 락을 얻게 되면, 그 락에 대한 식별자도 필요할 것이다. 그렇기에 LockId를 반환하는 것을 볼 수 있고, 필자는 이를 아래와 같이 구현했다. + +![](https://i.imgur.com/GylyaL2.png) + +다음은 오프라인 선점 방식을 적용한 예이다. + +![](https://i.imgur.com/YpQfrMi.png) + +![](https://i.imgur.com/yVVtbLI.png) + +잠금을 선점하는 데 실패하면 LockException이 발생하게 되고, 이를 서비스에서 마음껏 잘 활용하면 된다. + +잠금 해제를 하는 플로우를 구현하면 다음과 같다. + +![](https://i.imgur.com/lLhs6Ye.png) + +![](https://i.imgur.com/JA5lA7K.png) + +### DB를 이용한 LockManager 구현 + +사실, 위의 오프라인 선점 방식은 자체적으로도 구현할 수 있을 것 같다. + +![](https://i.imgur.com/mj6yMdO.png) + +위와 같이 Table을 두고, 락을 걸고 싶으면 락을 insert 해주면 된다. + +근데, 당연히 락을 insert할 때에도 락을 활용해서 동시성 문제를 해결해야 하지 않을까 싶다. + +![](https://i.imgur.com/HIhopsF.png) + +![](https://i.imgur.com/0ASVYHy.png) + +그리고 Lock 정보를 위와 같은 엔티티로써 표현한다. + +그리고 이를 Jdbc Template(DB를 더 쉽게 다룰 수 있도록 도와주는 라이브러리)을 활용해 다음과 같이 구현할 수 있다. + +![](https://i.imgur.com/dylR3pV.png) + +![](https://i.imgur.com/6Xu3vqP.png) + +대충 대충 읽어보면 이미 Lock이 존재하는지 확인하고 그렇지 않다면, Lock의 ID를 UUID를 통해서 만들고, locking을 건다. 그냥 락을 거는 로직이라고 보면 된다. + +다만, 이전에 SpringLockManager가 구현하는 것을 Spring JDBC Template을 활용해 직접 구현했다고 보면 된다.