Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Week 7] 애그리거트 트랜잭션 관리 - 김재연 #44

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions chap08/김재연.md
Original file line number Diff line number Diff line change
@@ -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을 활용해 직접 구현했다고 보면 된다.