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

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

애그리거트와 트랜잭션

  • 한 애그리거트를 두 사용자가 동시에 변경할 때 트랜잭션 이 필요하다

  • 애그리거트에 대해 사용할 수 있는 대표적인 트랜잭션 처리 방식에는 선점(Pessimistic) 잠금비선점(Optimistic) 잠금 두 가지 방식이 있다

선점 잠금

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

    • 잠금된 동안 블로킹 된다

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

    • 선점 잠금을 이용해서 트랜잭션 충돌 문제 를 해결한다

  • 선점 잠금은 보통 DMBS가 제공하는 행단위 잠금을 이용해서 구현한다

  • JPA Provider와 DBMS에 따라 잠금 모드 구현이 다른데, 스프링 데이터 JPA@Lock annotation을 사용해서 잠금 모드를 지정한다

선점 잠금과 교착 상태

  • 선점 잠금을 사용할 때는 잠금 순서에 따른 교착 상태 가 발생하지 않도록 주의해야 한다

  • 교착 상태 발생을 막기 위해 잠금을 구할 때 최대 대기 시간 을 지정해야 한다

    • 스프링 데이터 JPA@QueryHints annotation을 사용해서 쿼리 힌트 를 지정할 수 있다

비선점 잠금

  • 선점 잠금으로 모든 트랜잭션 충돌 문제가 해결되지는 않는다

    • 이때 필요한 것이 비선점 잠금 이다

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

  • 비선점 잠금을 구현하려면 애그리거트에 버전으로 사용할 숫자 타입 프로퍼티 를 추가해야 한다

  • 애그리거트를 수정할 때마다 버전으로 사용할 프로퍼티 값이 1씩 증가하는데, 이때 다음과 같은 쿼리를 사용한다

    UPDATE aggtable SET version = version + 1, colx = ?, coly = ?
    WHERE aggid = ? and version = 현재버전
    • 이 쿼리는 수정할 애그리거트와 매핑되는 테이블의 버전 값이 현재 애그리거트의 버전과 동일한 경우에만 데이터를 수정한다

    • 수정에 성공하면 버전 값을 1 증가시킨다

    • 다른 트랜잭션이 먼저 데이터를 수정해서 버전 값이 바뀌면, 데이터 수정에 실패하게 된다

  • JPA는 버전 을 이용한 비선점 잠금 기능을 지원한다

    • 버전으로 사용할 필드에 @Version annotation을 붙이고, 매핑되는 테이블에 버전을 저장할 칼럼을 추가한다

      • ex)

        @Version
        private long version;
    • JPA는 엔티티가 변경되어 UPDATE 쿼리를 실행할 때, @Version 에 명시한 필드를 이용해서 비선점 잠금 쿼리를 실행한다

    • 응용 서비스는 버전에 대해 알 필요가 없다!

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

      • 기능 실행 과정에서 애그리거트가 변경되면, JPA는 트랜잭션 종료 시점에 비선점 잠금을 위한 쿼리를 실행한다

  • 비선점 잠금을 위한 쿼리를 실행할 때, 실행 결과로 수정된 행의 개수가 0이면, 이미 누군가 앞서 데이터를 수정한 것이다

    • 이는 트랜잭션이 충돌 한 것이므로, 트랜잭션 종료 시점에 Exception 이 발생한다

    • 트랜잭션 충돌이 발생하면 OptimisticLockingFailureException 이 발생한다

      • 표현 영역 코드는 이 Exception이 발생했는지에 따라 트랜잭션 충돌이 일어났는지 확인할 수 있다

  • 응용 서비스에서 버전 일치 여부를 확인하는 method를 두고, 일치하지 않으면 버전이 충돌했다는 Exception 을 발생시켜 표현 계층에 이를 알린다

    • VersionConflictException

  • 표현 계층은 버전 충돌 Exception이 발생하면 버전 충돌 을 사용자에게 알려, 사용자가 알맞은 후속 처리 를 할 수 있도록 한다

  • 비선점 잠금과 관련해서 발생하는 두 개의 Exception

    • 하나는 스프링 프레임워크가 발생시키는 OptimisticLockingFailureException이고,

    • 다른 하나는 응용 서비스에서 발생시키는 VersionConflictException 이다

  • 이 두 Exception은 개발자 입장에서는 트랜잭션 충돌이 발생한 시점 을 명확하게 구분해 준다

    • VersionConflictException 은 이미 누군가가 애그리거트를 수정했다는 것을 의미하고,

    • OptimisticLockingFailureException 은 누군가가 거의 동시에 애그리거트를 수정했다는 것을 의미한다

  • 버전 충돌 상황에 대한 구분이 명시적으로 필요 없다면, 응용 서비스에서 프레임워크용 Exception을 발생시키는 것도 고려할 수 있다

    // 프레임워크가 제공하는 비선점 트랜잭션 충돌 관련 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. 잠금을 선점하지 않은 사용자가 기능을 실행했다면, 기능 실행을 막아야 한다

Last updated