> For the complete documentation index, see [llms.txt](https://chloe-codes1.gitbook.io/book-reviews/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://chloe-codes1.gitbook.io/book-reviews/ddd-start/08_-_-_.md).

# 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. 잠금을 선점하지 않은 사용자가 기능을 실행했다면, 기능 실행을 막아야 한다
