도메인 주도 개발 시작하기 (8) - 애그리거트 트랜잭션 관리

Jaewoo Ha·2022년 12월 11일
0

8.1 애그리거트와 트랜잭션

  • 개념적으로 동일한 애그리거트지만 물리적으로 서로 다른 래그리거트 객체를 사용한다. 서로의 애그리거트 객체에 영향을 주지 않는다.

두 스레드는 각각 트랜잭션을 커밋할 때 수정한 내용을 DB에 반영한다. 이 순서의 문제점은 운영자는 기존 배송지 정보를 이용해서 배송 상태로 변경했지만, 고객은 배송지 정보를 변경했다는 점이다.

-> 래그리거트의 일관성이 깨진다.

트랜잭션과의 연관성

  • DBMS가 지원하는 트랜잭션과 함께 애그리거트를 위한 추가적인 트랜잭션 처리 기법 필요
  • 선점 잠금과 비선점 잠금 방식으로 문제점 해결

8.2 선점 잠금

  • 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드가 해당 애그리거트를 수정하지 못하게 막는다.

동작 방식

  1. 스레드2는 스레드1이 애그리거트에 대한 잠금을 해제할 때까지 블로킹이 된다.
  2. 애그리거트를 수정하고 트랜잭션을 커밋하면 잠금을 해제한다. 이 순간 대기하고 있던 스레드2가 래그리거트에 접근하게 된다.
  3. 스레드1이 트랜잭션을 커밋한 뒤에 스레드2가 애그리거트를 구하게 되므로 스레드2는 스레드1이 수정한 애그리거트의 내용을 보게 된다.

장점

  • 한 스레드가 애그리거트를 구하고 수정하는 동안 다른 스레드가 수정할 수 없으므로 동시에 애그리거트르 수정할 때 발생하는 데이터 충돌 문제 해소할 수 있다.

8.2.1 선점 잠금과 교착 상태

  • 상대방 스레드가 먼저 선점한 잠금을 구할 수 없어 교착 상태에 빠지게 되는 경우가 있다.
  • 사용자 수가 많을 때 발생할 가능성이 높고 사용자 수가 많아지면 교착 상태에 빠지는 스레드가 더 빠르게 증가한다.

해결 방법

  • 잠금을 구할 때 최대 대기 시간을 지정할 수 있다. JPA에서 선점 잠금을 시도할 때 최대 대기 시간을 지정하려면 hints를 사용한다.
hints.put("javax.persistence.lock.timeout", 2000)
  • @QueryHints 애너테이션을 사용해서 쿼리 힌트를 지정할 수 있다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHint({
	@QueryHint(name = "javax.persistence.lock.timeout", value = "2000")
})

8.3 비선점 잠금

  • 선점 잠금으로 모든 트랜잭션 충돌 문제가 해결되는 것은 아니다.
  • 동시에 접근하는 것을 막는 대신 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식

애그리거트에 버전으로 사용할 숫자 타입 프로퍼티를 추가한다음 애그리거트를 수정할 때마다 버전으로 사용할 프로퍼티 값이 1씩 증가한다.
수정할 애그리거트와 매핑되는 테이블의 버전 값이 현재 애그리거트의 버전과 동일한 경우에만 데이터를 수정한다. 다른 트랜잭션이 먼저 데이터를 수정해서 버전 값이 바뀌면 데이터 수정에 실패하게 된다.

8.4 오프라인 선점 잠금

  • 동시에 수정하는 것을 막고 싶을 때 사용한다.

동작 방식

  • 여러 트랜잭션에 걸쳐 동시 변경을 막는다.
  • 첫 번째 트랜잭션을 시작할 때 오프라인 잠금을 선점하고, 마지막 츠랜잭션에서 잠금을 해제한다.
  • 잠금 유효 시간을 가진다.

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

오프라인 선점 잠금은 크게 잠금 선점 시도, 잠금 확인, 잠금 해제, 잠금 유효시간 연장의 네 가지 기능이 필요하다.

interface LockManager {
    fun tryLock(type: String, id: String): LockId
    
    fun heckLock(lockId: LockId)
    
    fun releaseLock(lockId: LockId)
    
    fun extendLockExpiration(lodckId: LockId, inc: Long)
}

잠금을 구하면 잠금을 해제하거나 잠금이 유효한지 검사하거나 잠금 유효 시간을 늘릴 때 LockId를 사용한다.

class Lock(private var _value: String) {
    
    fun LockId(value: String) {
        this._value = value
    }
    
    val value: String
        get() = _value.ifEmpty { "" }
}

오프라인 선점 잠금이 필요한 코드는 LockManager#tryLock()을 이용해서 잠금을 시도한다.

profile
내일의 코드는 더 안전하고 깔끔하게

0개의 댓글