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를 갖는 잠금이 유효한지 검사해야한다.
- 잠금의 유효 시간이 지났으면 이미 다른 사용자가 잠금을 선점한다.
- 잠금을 선점하지 않은 사용자가 기능을 실행했다면 기능 실행을 막아야 한다.
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;
- 추가 자세한 이용방법은 SpringLockManager 참조
'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 |
Uploaded by Notion2Tistory v1.1.0