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] 애그리거트 트랜잭션 관리 - 최인준 #41

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
111 changes: 111 additions & 0 deletions chap08/최인준.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
## 애그리거트와 트랜잭션

<img src="https://file.notion.so/f/f/412c9b0a-ec55-45df-8458-890f1703ea17/994076eb-38d8-4d43-9915-33068ff6575b/Untitled.png?id=93bd1460-a080-40a0-a79d-e756bbf8b322&table=block&spaceId=412c9b0a-ec55-45df-8458-890f1703ea17&expirationTimestamp=1722420000000&signature=2ksMGqVfGygekUSihJtJGeDsEaHMvb3VhskucI8terg&downloadName=Untitled.png" width="50%" height="50%">

위 그림과 같은 상황이 있다고 해보자.

운영자 스레드와 고객 스레드는 개념적으로 동일한 애그리거트(주문)지만 **물리적으로** 서로 다른 애그리거트 객체를 사용한다.

위 그림의 문제는 운영자는 배송중 상태로 변경했는데 고객은 그 사이 배송지 정보를 변경했다. 결국 고객은 원하는 상품을 이전에 설정해놓았던 배송지로 배송받게 된다.

이는 일관성이 깨지므로 문제이고 다음 두 가지 중 하나로 해결해야 한다.

- 운영자가 배송지 정보를 조회하고 상태를 변경하는 동안, 고객이 애그리거트를 수정하지 못하게 막는다.
- 운영자가 배송지 정보를 조회한 이후에 고객이 정보를 변경하면, 운영자가 애그리거트를 다시 조회한 뒤 수정하도록 한다.

이 두가지는 애그리거트를 위한 추가적인 트랜잭션 처리 기법이 필요하다.

트랜잭션 처리 방식에는 **선점 잠금과 비선점 잠금이 있다.**

## 선점 잠금

선점 잠금은 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드가 해당 애그리거트를 수정하지 못하게 막는 방식이다. 다음 그림 예시와 같다.

<img src="https://file.notion.so/f/f/412c9b0a-ec55-45df-8458-890f1703ea17/232ba6f3-9e07-4e94-9459-367a55d9f46b/Untitled.png?id=11bad4f7-1ad6-4e15-a60b-e64352e303e6&table=block&spaceId=412c9b0a-ec55-45df-8458-890f1703ea17&expirationTimestamp=1722420000000&signature=JH-ttps9bXoxf_acHT1EcMv_aQnmztMyGFR6eISCMZI&downloadName=Untitled.png" width="50%" height="50%">


스레드1이 애그리거트를 수정하고 트랜잭션을 커밋하면 잠금을 해제한다.

이후에는 스레드2가 애그리거트에 접근하게 된다.

선점 잠금은 보통 DBMS가 제공하는 행단위 잠금을 사용해서 구현한다.

오라클을 비롯한 다수의 DBMS가 for update와 같은 쿼리를 사용해서 특정 레코드에 한 커넥션만 접근할 수 있는 잠금장치를 제공한다.

스프링 데이터 JPA에서는 @Lock 어노테이션을 사용해서 잠금 모드를 지정한다.

```java
public interface MemberRepository extends Repository<Member, Long> {
@Lock(LockmodeType.PESSIMISTIC_WRITE)
@Query("select m from Member m where m.id = :id")
Optional<Member> findByIdForUpdate(@Param("id") Long id);
}
```

### 선점 잠금과 교착 상태

선점 잠금 기능을 사용할 때는 데드락이 발생하지 않도록 특히 주의해야 한다.

다음과 같은 상황이 있다고 해보자

1. 스레드1: A 애그리거트에 대한 선점 잠금 구함
2. 스레드2: B 애그리거트에 대한 선점 잠금 구함
3. 스레드1 : B 애그리거트에 대한 선점 잠금 시도
4. 스레드2: A 애그리거트에 대한 선점 잠금 시도

이 순서에 따르면 서로 상대방 스레드가 먼저 선점한 잠금을 구할 수 없어 데드락에 빠진다.

이는 사용자 수가 많을 때 발생할 가능성이 높고, 사용자 수가 많아지면 데드락에 걸리는 스레드는 더 빠르게 증가한다.

→ 이런 문제가 발생하지 않도록 하려면 잠금을 구할 때 최대 대기 시간을 지정해야 한다.

→ 최대 대기 시간이 지나도 잠금을 못 구하면 익셉션을 발생시킨다.

## 비선점 잠금

선점 잠금이 강력해 보이긴 하지만 선점 잠금으로 모든 트랜잭션 충돌 문제가 해결되는 것은 아니다.

다음 그림 예시를 보자.

<img src="https://file.notion.so/f/f/412c9b0a-ec55-45df-8458-890f1703ea17/eb88dcb8-d63b-4515-8f3f-6af162ff3c5f/Untitled.png?id=fd7f4eb0-8056-437f-81ec-1854c57be56b&table=block&spaceId=412c9b0a-ec55-45df-8458-890f1703ea17&expirationTimestamp=1722420000000&signature=BO0RUgNwH9asToU62IaihVtxsh57x5EeBPoqTbmfV_w&downloadName=Untitled.png" width="50%" height="50%">


위 예시에서 문제는 운영자가 배송지 정보를 조회하고 배송 상태로 변경하는 사이에 고객이 배송지를 변경한다는 것이다.

이 문제는 선점 잠금으로는 해결할 수 없다. 비선점 잠금으로 해결해야 한다.

비선점 잠금은 동시에 접근하는 것을 막는 대신 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식이다.

비선점 잠금은 애그리거트의 버전 프로퍼티를 활용하여 수정할 애그리거트의 버전 값이 현재 애그리거트의 버전과 동일한 경우에만 데이터를 수정한다. 그림 예시는 다음과 같다.

<img src="https://file.notion.so/f/f/412c9b0a-ec55-45df-8458-890f1703ea17/a4a8cece-3820-4725-8191-ece7d844f49d/Untitled.png?id=beef7274-d6dd-4ffa-b2d2-be725a7d4817&table=block&spaceId=412c9b0a-ec55-45df-8458-890f1703ea17&expirationTimestamp=1722420000000&signature=ELDgD1b9aAR2PPYpCmyFAHzU_OuCNmbigU4lKjDuN6c&downloadName=Untitled.png" width="50%" height="50%">

비선점 잠금을 활용할 때 응용 서비스는 버전에 대해 알 필요가 없다.

리포지터리에서 필요한 애그리거트를 구하고 알맞은 기능만 실행하면 된다.

비선점 잠금을 위한 쿼리를 실행할 때 쿼리 실행결과로 수정된 행의 개수가 0이면 이미 누군가 앞서 데이터를 수정한 것이고 이는 트랜잭션이 충돌한 것이므로 익셉션이 발생한다.

### 강제 버전 증가

애그리거트 루트 외에 다른 엔티티가 존재하는데 기능 실행 도중 루트가 아닌 다른 엔티티의 값만 변경된다고 해보자. 이 경우엔 버전을 어떻게 해야할까? 결론은 루트 애그리거트의 버전 값도 증가해야 한다.

루트 엔티티의 값이 바뀐게 아니어도 애그리거트 구성요소 중 일부 값이 바뀐거면 논리적으로 그 애그리거트는 바뀐 것이다.

→ JPA는 이를 처리하기 위해 엔티티를 구할 때 강제로 버전 값을 증가시키는 잠금 모드를 지원한다.

## 오프라인 선점 잠금

여러 사람이 동시에 문서 편집화면을 볼 수 있을 때 한 사람이 수정하고 있다면 다른 사람은 수정하는 것을 엄격하게 막기 위해선 **오프라인 선점 잠금**이 필요하다.

단일 트랜잭션에서 동시 변경을 막는 선점 잠금 방식과 달리 오프라인 선점 잠금은 여러 트랜잭션에 걸쳐 동시 변경을 막는다.

수정기능을 예로 생각해보자. 수정 기능은 보통 두 개의 트랜잭션으로 구성되고 예시는 다음과 같다.

<img src="https://file.notion.so/f/f/412c9b0a-ec55-45df-8458-890f1703ea17/8d6ccf61-8dab-4e76-96a3-f4710745e49c/Untitled.png?id=7bebf020-dcf5-4c3b-ac2c-838c42af953c&table=block&spaceId=412c9b0a-ec55-45df-8458-890f1703ea17&expirationTimestamp=1722420000000&signature=0aO5Ytw6YmBFWiUV-H46B0GYYzD0I1Ljf4mLT3ZJ9mM&downloadName=Untitled.png" width="50%" height="50%">

만약 A가 과정 3의 수정 요청을 수행하지 않고 프로그램을 종료한다면?

다른 사용자는 영원히 잠금을 구할 수 없는 상황이 발생한다.

그렇기에 오프라인 선점 잠금 방식은 잠금 유효 시간을 가져야 하고 자동으로 잠금이 해제되도록 해야 한다.