8.1 애그리거트와 트랜잭션
- 애그리거트에 대해 사용할 수 있는 대표적인 트랜잭션 처리 방식
8.2 선점 잠금
- 선점 잠금(Pessimistic Lock)은 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드가 해당 애그리거트를 수정하지 못하게 막는 방식
- 먼저 사용하고 있는 스레드가 애그리거트에 대한 잠금을 해제할 때까지 다른 스레드는 블로킹(Blocking)됨
- 사용한 스레드가 애그리거트 수정 후 트랜잭션 커밋하면 잠금 해제
- 선점잠금은 보통 DBMS가 제공하는 행단위 잠금 사용해서 구현
- JPA EntityManager는
LockModeType
을 인자로 받는 find()
메서드 제공
LockModeType.PESSIMISTIC_WRITE
를 전달
- 스프링 데이터 JPA는
@Lock
애너테이션 사용해서 잠금 모드 지정
8.2.1 선점 잠금과 교착 상태
- 선점 잠금 기능을 사용할 때는 잠금 순서에 따른 교착 상태(deadlock)가 발생하지 않도록 주의
- 선점 잠금에 따른 교착 상태는 상대적으로 사용자 수가 많을 때 발생할 가능성 높음
- 사용자 수가 많아지면 교착 상태에 빠지는 스레드 더 빠르게 증가
- 잠금 구할 때 최대 대기 시간 지정해서 이런 문제 발생 방지
- JPA에서는 힌트 사용
Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.lock.timeout", 2000);
Order order = entityManager.find(
Order.class, orderNo, LockModeType.PESSIMISTIC_WRITE, hints);
- 스프링 데이터 JPA는
@QueryHints
애너테이션 사용해서 쿼리 힌트 지정
@QueryHints({@QueryHints(name = "javax.persistence.lock.timeout", value = "2000"})
8.3 비선점 잠금
- 선점 잠금이 강력해 보이지만 선점 잠금만으로 모든 트랜잭션 충동 문제 해결 되는 것은 아님
- 비선점 잠금
- 동시에 접근하는 것을 막는 대신 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식
- 애그리거트에 버전으로 사용할 숫자 타입 프로퍼티 추가해야 함
- 애그리거트 수정할 때마다 버전으로 사용할 프로퍼티 값 1씩 증가
UPDATE aggtable SET version = version + 1, colx = ?, coly = ?
WHERE aggid = ? and version = 현재버전
- 쿼리 분석
- 수정할 에그리거트와 매핑되는 테이블의 버전 값이 현재 애그리거트의 버전과 동일한 경우에만 데이터 수정
- 수정에 성공하면 버전 값 1 증가
- JPA는 버전을 이용한 비선점 잠금 기능 지원
- 버전으로 사용할 필드에
@Version
애너테이션 붙이고 매핑되는 테이블에 버전을 저장할 칼럼 추가
- 엔티티가 변경되어 UPDATE 쿼리 실행할 때 @Version 명시한 필드 이용해서 비선점 잠금 퉈리 실행
- 응용 서비스는 버전에 대해 알 필요 없음. 리포지터리에서 필요한 애그리거트 구하고 알맞은 기능만 실행하면 됨. 애그리거트 데이터 변경되면 JPA는 트랜잭션 종료 시점에 비선점 잠금을 위한 쿼리 실행
- 비선점 잠금을 위한 쿼리 실행할 때 쿼리 실행 결과로 수정된 행의 개수가 0이면 이미 누가 데이터 수정한 것 -> 트랜잭션 종료 시점에 익셉션 발생 ->
OptimisticLockingFailureException
발생
- 사용자가 폼을 서버에 전송할 때 함께 전송한 버전과 애그리거트 버전이 동일한 경우에만 애그리거트 수정 기능 수행하도록 함 -> 트랜잭션 충동 문제 해소
- 비선점 잠금 방식을 여러 트랜잭션으로 확장하려면 애그리거트 정보를 뷰로 보여줄 때 버전 정보도 함께 사용자 화면에 전달
- HTML 폼 생성하는 경우 버전 값을 갖는 hidden 타입
<input>
태그 생성해서 버전 값 서버에 함께 전달
- 응용 서비스에 전달할 요청 데이터는 사용자가 전송한 버전 값 포함
- 표현 걔층은 버전 충돌 익셉션이 발생하면 버전 충돌을 사용자에게 알려 사용자가 알맞은 후속 처리를 할 수 있도록 함
- 버전 충돌 상황에 대한 구분이 명시적으로 필요 없다면 응용 서비스에서 프레임워크용 익셉션 발생시키는 것도 고려 가능
8.3.1 강제 버전 증가
- 애그리거트 루트 외에 다른 엔티티가 존재하는 애그리거트가 기능 실행 도중 루트가 아닌 다른 엔티티의 값만 변경된 경우 -> JPA는 루트 엔티티의 버전 값 증가 시키지 않음 -> 강제 버전 증가 필요
EntityManager#find()
메서드로 엔티티를 구할 때 강제로 버전 값을 증가시키는 잠금 모드 지원
LockModeType.OPTIMISTIC_FORCE_INCREMENT
를 사용하면 해당 엔티티의 상태 변경에 상관없이 트랜잭션 종료 시점에 버전 값 증가 처리
8.4 오프라인 선점 잠금
- 오프라인 선점 잠금 방식(Offline Pessimistic Lock)
- 여러 트랜잭션에 걸쳐 동시 변경을 막음
- 첫 번째 트랜잭션 시작할 때 오프라인 잠금을 선점하고, 마지막 트랜잭션에서 잠금 해제
- 잠금 해제 전까지 다른 사용자는 잠금 구할 수 없음
- 잠금을 해제하지 않고 프로그램 종료될 수 있으므로 유효 시간 지나면 자동으로 잠금해제하도록 잠금 유효 시간 가져야 함
- 일정 주기로 유효 시간 증가키시는 방식 필요
8.4.1 오프라인 선점 잠금을 위한 LockManager 인터페이스와 관련 클래스
- 오프라인 선점 잠금은 크게
잠금 선점 시도
, 잠금 확인
, 잠금 해제
, 잠금 유효시간 연장
의 4가지 기능 필요
package com.myshop.lock;
public interface LockManager {
LockId tryLock(String type, String id) throws LockException;
void checkLock(LockId lockId) throws LockException;
void releaseLock(LockId lockId) throws LockException;
void extendLockExpiration(LockId lockId, long inc) throws LockException;
}
8.4.2 DB를 이용한 LockManager 구현
예시 코드는 책 참고하기