https://www.notion.so/8-2-43d0ddc04fad4909a763063ab821a709

강제 버전 증가

 

  • 애그리것에 애그리것 루트 외에 다른 엔티티가 존재하는데 기능 실행 도중 루트가 아닌 다른 엔티티의 값만 변경된다고 하자. 이 경우 JPA는 루트 엔티티의 버전 값을 증가하지 않는다.
  • 연관된 엔티티의 값이 변경된다고 해도 루트 엔티티 자체의 값은 바뀌는 것이 없으므로 루트 엔티티의 버전값을 갱신하지는 않는 것이다.
  • 근데 비록 루트 엔티티의 값이 바뀌지 않았더라도 애그리것의 구성 요소중 일부값이 바뀌면 논리적으로 그 애그리것은 바뀐 것이다.
  • 따라서 애그리것 내에 어떤 구성요소의 상태가 바뀌면 루트 애그리것의 버전 값을 증가해야 비 선점 잠금이 올바르게 동작한다.
  • JPA는 이런 문제를 처리할 수 있도록 EntityManager find() 메서드로 엔티티를 구할 때 강제로 버전 값을 증가시키는 잠금 모드를 지원하고 있다.
  • 다음은 비선점 강제 버전 증가 잠금 모드를 사용해서 엔티티를 구하는 코드의 예제이다.
		public Order findByIdOptimisticLockMode(OrderNo id) {
        Map<String, Object> hints = new HashMap<>();
        hints.put("javax.persistence.lock.timeout", 2000);
        return entityManager.find(Order.class, id, LockModeType.OPTIMISTIC_FORCE_INCREMENT, hints);
    }
  • OPTIMISTIC_FORCE_INCREMENT를 사용하면 해당 엔티티의 상태가 변경되었는지 여부에 상관없이 트랜잭션 종료 시점에 버전 값 증가 처리를 한다.

 

오프라인 선점 잠금

  • 단일 트랜잭션에서 동시에 변경을 막는 선점 잠금 방식과 달리 오프라인 선점 잠금은 여러 트랜잭션에 걸쳐 동시 변경을 막는다.
  • 첫번째 트랜잭션을 시작할 때 오프라인 잠금을 선점하고, 마지막 트랜잭션에서 잠금을 해제한다.
  • 잠금을 해제하기 전까지 다른 사용자는 잠금을 구할 수 없다.

 

  • 예를 들어 수정 기능을 생각해보자, 보통 수정 기능은 두 개의 트랜잭션으로 구성된다.
  • 첫번째 트랜잭션은 폼을 보여주고, 두번째 트랜잭션은 데이터를 수정한다.
  • 오프라인 선점 잠금을 사용하면 폼 요청과정에서 잠금을 선점하고 수정과정에서 잠금을 해제한다.
  • 이미 잠금을 선점한 상태에서 다른 사용자가 폼을 요청하면 과정 2처럼 잠금을 구할 수 없어 에러 화면을 보게 된다.

출처: https://incheol-jung.gitbook.io/docs/study/ddd-start/8

 

  • 위의 그림에서 사용자 A가 과정 3의 수정 요청을 수행하지 않고 프로그램을 종료하면 어떻게 될까 ?
  • 이 경우 잠금을 해제하지 않으므로 다른 사용자는 영원히 잠금을 구할 수 없는 상황이 발생한다.
  • 이런 사태를 방지하기 위해 오프라인 선점 방식은 잠금의 유효 시간을 가져야 한다.
  • 유효시간이 지나면 자동으로 잠금을 해제해서 다른 사용자가 잠금을 일정 시간 후에 다시 구할 수 있도록 해야 한다.

 

  • 사용자 A는 잠금 유효시간이 지난 후 1초 뒤에 3번 과정을 수행했다고 가정하자
  • 잠금이 해제되어 사용자 A는 수정에 실패하게 된다.
  • 이런 상황을 만들지 않으려면 일정 주기로 유효시간을 증가시키는 방식이 필요하다.
  • 예를 들어 수정품에서 1분 단위로 ajax 호출을 해서 잠금 유효 시간을 1분씩 증가시키는 방법이 있다.

 

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

  • 오프라인 선점 잠금은 크게 잠금 선점 시도, 잠금 확인, 잠금 해제, 락 유효시간 연장의 네가지 기능을 제공 해야한다.
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;
}

 

public class LockId {
    private String value;

    public LockId(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

 

@RequestMapping("/some/edit/{id}")
public String editForm(@PathVariable("id") Long id, ModelMap model) {
	// 1. 오프라인 선점 잠금 시도
	LockId lockId = lockManager.tryLock("data", id);

	// 2. 기능 실행
	Data data = someDao.select(id);
	model.addAttribute("data", data);
	
	// 3. 잠금 해제에 사용할 LockId를 모델에 추가
	model.addAttribute("lockId", lockId);

	return "editForm";
}

 

  • 잠금을 해제할 경우 전달받은 LockId를 이용한다.

 

  • 잠금시 반드시 주어진 lockId를 갖는 잠금이 유효한지 검사해야한다.
    1. 잠금의 유효 시간이 지났으면 이미 다른 사용자가 잠금을 선점한다.
    1. 잠금을 선점하지 않은 사용자가 기능을 실행했다면 기능 실행을 막아야 한다.

DB를 이용한 LockManager 구현

  • 잠금 정보를 저장할 테이블과 인덱스를 생성한다.
  • 쿼리는 MySQL용
CREATE TABLE LOCKS (
    `type` varchar(255),
    id varchar(255),
    lockid varchar(255),
    expiration_time datetime,
    primary key (`type`, id)
) character set utf8;

 

https://www.notion.so/8-2-43d0ddc04fad4909a763063ab821a709

'book > DDD start' 카테고리의 다른 글

11장 CQRS  (0) 2021.07.31
10장 이벤트  (0) 2021.07.28
8장 애그리것 트랜잭션 관리  (0) 2021.07.07
9장 도메인 모델과 바운디드 컨텍스트 2부  (0) 2021.06.30
9장 도메인 모델과 바운디드 컨텍스트 1부  (0) 2021.06.30

+ Recent posts