# Chapter 8: 애그리거트 트랜잭션 관리

<br>

## 💡 책에서 기억하고 싶은 내용

<br>

### 애그리거트와 트랜잭션

* 한 애그리거트를 두 사용자가 동시에 변경할 때 `트랜잭션` 이 필요하다
* 애그리거트에 대해 사용할 수 있는 대표적인 트랜잭션 처리 방식에는 `선점(Pessimistic) 잠금` 과 `비선점(Optimistic) 잠금` 두 가지 방식이 있다

### 선점 잠금

* 먼저 애그리거트를 구한 스레드가 `애그리거트 사용이 끝날 때까지` 다른 스레드가 해당 애그리거트를 수정하지 못하게 막는 방식
  * 잠금된 동안 `블로킹` 된다
* 한 스레드가 애그리거트를 구하고 수정하는 동안 다른 스레드가 수정할 수 없으므로, 동시에 애그리거트를 수정할 때 발생하는 `데이터 충돌 문제` 를 해소할 수 있다
  * 선점 잠금을 이용해서 `트랜잭션 충돌 문제` 를 해결한다
* 선점 잠금은 보통 DMBS가 제공하는 행단위 잠금을 이용해서 구현한다
* JPA Provider와 DBMS에 따라 잠금 모드 구현이 다른데, `스프링 데이터 JPA` 는 `@Lock` annotation을 사용해서 잠금 모드를 지정한다

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

* 선점 잠금을 사용할 때는 잠금 순서에 따른 `교착 상태` 가 발생하지 않도록 주의해야 한다
* 교착 상태 발생을 막기 위해 잠금을 구할 때 `최대 대기 시간` 을 지정해야 한다
  * `스프링 데이터 JPA` 는 `@QueryHints` annotation을 사용해서 `쿼리 힌트` 를 지정할 수 있다

### 비선점 잠금

* 선점 잠금으로 모든 트랜잭션 충돌 문제가 해결되지는 않는다
  * 이때 필요한 것이 `비선점 잠금` 이다
* 비선점 잠금 은 동시에 접근하는 것을 막는 대신, 변경한 데이터를 `DBMS에 반영하는 시점` 에 `변경 가능여부를 확인` 하는 방식이다
* 비선점 잠금을 구현하려면 애그리거트에 `버전`으로 사용할 `숫자 타입 프로퍼티` 를 추가해야 한다
* 애그리거트를 수정할 때마다 버전으로 사용할 프로퍼티 값이 1씩 증가하는데, 이때 다음과 같은 쿼리를 사용한다

  ```sql
  UPDATE aggtable SET version = version + 1, colx = ?, coly = ?
  WHERE aggid = ? and version = 현재버전
  ```

  * 이 쿼리는 `수정할 애그리거트`와 매핑되는 `테이블의 버전 값`이 현재 애그리거트의 버전과 `동일한 경우`에만 데이터를 수정한다
  * 수정에 성공하면 버전 값을 1 증가시킨다
  * 다른 트랜잭션이 먼저 데이터를 수정해서 버전 값이 바뀌면, 데이터 수정에 실패하게 된다
* JPA는 `버전` 을 이용한 비선점 잠금 기능을 지원한다
  * 버전으로 사용할 필드에 `@Version` annotation을 붙이고, 매핑되는 테이블에 버전을 저장할 칼럼을 추가한다
    * ex)

      ```java
      @Version
      private long version;
      ```
  * JPA는 엔티티가 변경되어 UPDATE 쿼리를 실행할 때, `@Version` 에 명시한 필드를 이용해서 비선점 잠금 쿼리를 실행한다
  * 응용 서비스는 버전에 대해 알 필요가 없다!
    * 리포지터리에서 필요한 애그리거트를 구하고, 알맞은 기능만 실행하면 된다
    * 기능 실행 과정에서 애그리거트가 변경되면, JPA는 트랜잭션 종료 시점에 비선점 잠금을 위한 쿼리를 실행한다
* 비선점 잠금을 위한 쿼리를 실행할 때, 실행 결과로 수정된 행의 개수가 0이면, 이미 누군가 앞서 데이터를 수정한 것이다
  * 이는 트랜잭션이 `충돌` 한 것이므로, 트랜잭션 종료 시점에 `Exception` 이 발생한다
  * 트랜잭션 충돌이 발생하면 `OptimisticLockingFailureException` 이 발생한다
    * `표현 영역` 코드는 이 `Exception`이 발생했는지에 따라 `트랜잭션 충돌`이 일어났는지 `확인`할 수 있다
* 응용 서비스에서 버전 일치 여부를 확인하는 method를 두고, 일치하지 않으면 버전이 충돌했다는 `Exception` 을 발생시켜 `표현 계층`에 이를 알린다
  * `VersionConflictException`
* 표현 계층은 버전 충돌 Exception이 발생하면 `버전 충돌` 을 사용자에게 알려, 사용자가 알맞은 `후속 처리` 를 할 수 있도록 한다
* `비선점 잠금과 관련해서 발생하는 두 개의 Exception`
  * 하나는 스프링 프레임워크가 발생시키는 `OptimisticLockingFailureException`이고,
  * 다른 하나는 응용 서비스에서 발생시키는 `VersionConflictException` 이다
* 이 두 Exception은 개발자 입장에서는 `트랜잭션 충돌이 발생한 시점` 을 명확하게 구분해 준다
  * `VersionConflictException` 은 이미 누군가가 애그리거트를 수정했다는 것을 의미하고,
  * `OptimisticLockingFailureException` 은 누군가가 거의 동시에 애그리거트를 수정했다는 것을 의미한다
* 버전 충돌 상황에 대한 구분이 명시적으로 필요 없다면, 응용 서비스에서 프레임워크용 Exception을 발생시키는 것도 고려할 수 있다

  ```java
  // 프레임워크가 제공하는 비선점 트랜잭션 충돌 관련 Exception 사용
  throw new OptimisticLockingFailureException("version conflict");
  ```

### 강제 버전 증가

* 애그리거트에 애그리거트 루트 외에 다른 엔티티가 존재하는데 기능 실행 도중 루트가 아닌 다른 엔티티의 값만 변경될 경우, JPA는 `루트 앤티티`의 `버전 값`을 `증가시키지 않는다`
  * 연관된 엔티티의 값이 변경된다고 해도 루트 앤티티 자체의 값은 바뀌는 것이 없으므로, 루트 엔티티의 버전 값은 갱신하지 않는 것이다
* but, 이런 JPA 특징은 `애그리거트 관점`에서 보면 문제가 된다
  * 루트 엔티티의 값이 바뀌지 않았더라도, 애그리거트의 구성요소 중 일부 값이 바뀌면 논리적으로 그 애그리거트는 바뀐 것이다
* 따라서 애그리거트 내에 `어떤 구성요소의 상태가 바뀌면` 루트 애그리거트의 `버전 값이 증가`해야 비선점 잠금이 올바르게 동작한다
* JPA는 이런 문제를 처리할 수 있도록 `EntityManager#find()` method로 엔티티를 구할 대 강제로 버전 값을 증가시키는 `잠금 모드` 를 지원한다
  * `LockModeType.OPTIMISTIC_FORCE_INCREMENT` 를 사용하면 해당 엔티티의 상태가 변경되었는지에 상관없이 `트랜잭션 종료 시점` 에 버전 값 `증가` 처리를 한다
    * 이 잠금 모드를 사용하면 애그리거트 루트 앤티티가 아닌 다른 앤티티나 밸류가 변경되더라도 버전 값을 증가시킬 수 있으므로 비선점 잠금 기능을 안전하게 적용할 수 있다
* 스프링 데이터 JPA를 사용하면 `@Lock` annotation을 이용해서 지정하면 된다

### 오프라인 선전 잠금

* 오프라인 선전 잠금은 단일 트랜잭션에서 동시 변경을 막는 선점 잠금 방식과 달리, `여러 트랜잭션`에 걸쳐 `동시 변경`을 막는다
* 첫 번째 트랜잭션을 시작할 때 `오프라인 잠금`을 `선점`하고, 마지막 트랜잭션에서 잠금을 `해제`한다
  * 잠금을 해제하기 전까지 다른 사용자는 잠금을 구할 수 없다!
* 오프라인 선점 방식은 `잠금 유효 시간` 을 가져야 한다
  * 유효 시간이 지나면 자동으로 잠금을 해제해서 다른 사용자가 잠금을 일정 시간 후에 다시 구할 수 있도록 해야한다
  * ex)
    * 수정 폼에서 1분 단위로 Ajax 호출을 해서 잠금 유효 시간을 1분씩 증가시키기

### 오프라인 선점 잠금을 위한 LockManager 인터페이스와 관련 클래스

* 오프라인 선점 잠금은 네 가지 기능이 필요하다
  1. 잠금 선점 시도
  2. 잠금 확인
  3. 잠금 해제
  4. 잠금 유효시간 연장
* 위 기능을 위한 `LockManager` 인터페이스를 사용할 수 있다
* 잠금을 선점한 이후에 실행하는 기능은 아래의 상황을 고려하여 반드시 주어진 `LockId` 를 갖는 잠금이 유효한지 확인해야 한다
  1. 잠금 유효 시간이 지났으면 이미 다른 사용자가 잠금을 선점한다
  2. 잠금을 선점하지 않은 사용자가 기능을 실행했다면, 기능 실행을 막아야 한다


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://chloe-codes1.gitbook.io/book-reviews/ddd-start/08_-_-_.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
