: 아틀라시안의 컨플루언스는 문서를 편집할 때 누군가 먼저 편집을 진행하고 있다면 다른 사용자가 문서를 수정하고 있다는 안내 문구를 보여준다.
: 이런 안내를 통해 여러 사용자가 동시에 한 문서를 수정할 때 발생하는 충돌을 사전에 방지할 수 있게 해준다.
: 컨플루언스는 사전에 충돌 여부를 알려주지만 동시에 수정하는 것을 막진 않는다. 더 엄격하게 데이터 충돌을 막고 싶다면 누군가 수정 화면을 보고 있을 때 수정화면 자체를 실행하지 못하도록 해야 한다.
: 한 트랜잭션 범위에서만 적용되는 선점 잠금 방식이나 나중에 버전 충돌을 확인하는 비선점 잠금 방식으로는 이를 구현할 수 없다. 이때 필요한 것이 바로 오프라인 선점 잠금 방식이다.
: 단일 트랜잭션 동시 변경을 막는 선점 잠금 방식과 달리 오프라인 선점 잠금은 여러 트랜잭션에 걸쳐 동시 변경을 막는다.
: 첫 번째 트랜잭션을 시작할 때 오프라인 잠금을 선점하고 마지막 트랜잭션에서 잠금을 해제한다. 잠금을 해제하기 전까지 다른 사용자는 잠금을 구할 수 없다.
: 예를 들어 수정 기능을 생각해 보자, 보통 수정 기능은 두 개의 트랜잭션으로 구성된다.
•
첫 번째 트랜잭션은 폼을 보여주고 두 번재 트랜잭션은 데이터를 수정한다.
•
오프라인 선점 잠금을 사용하면 과정 1(과정1.1)처럼 폼 요청 과정에서 잠금을 선점하고 과정 3처럼 수정 과정에서 잠금을 해제한다.
•
이미 잠금을 선점한 상태에서 다른 사용자가 폼을 요청하면 과정 2처럼 잠금을 구할 수 없어 에러 화면을 보게 된다.
: 만약 사용자 A가 과정 3의 수정 요청을 수행하지 않고 프로그램을 종료하면 어떻게 될까? 이 경우 잠금을 해제하지 않으므로 다른 사용자는 영원히 잠금을 구할 수 없는 상황이 발생한다.
: 이런 사태를 방지하기 위해 오프라인 선점 방식은 잠금 유효 시간을 가져야 한다. 유효 시간이 지나면 자동으로 잠금을 해제해서 다른 사용자가 잠금을 일정 시간 후에 다시 구할 수 있도록 해야 한다.
•
사용자 A가 잠금 유효 시간이 지난 후 1초 뒤에 3번 과정을 수행했다고 가정하자.
•
잠금이 해제되어 사용자 A는 수정에 실패하게 된다. 이런 상황을 만들지 않으려면 일정 주기로 유효 시간을 증가시키는 방식이 필요하다.
•
예를 들어 수정 폼에서 1분 단위로 Ajax 호출을 해서 잠금 유효시간을 1분씩 증가시키는 방법이 있다.
1. 오프라인 선점 잠금을 위한 LockManager 인터페이스와 관련 클래스
: 오프라인 선점 잠금은 크게 잠금 선점 시도, 잠금 확인, 잠금 해제, 잠금 유효시간 연장의 네 가지 기능이 필요하다. 이 기능을 위한 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;
}
Java
복사
: tryLock() 메서드는 type과 id를 파라미터로 갖는다. 이 두 파라미터에는 각각 잠글 대상 타입과 식별자를 값으로 전달하면 된다.
: 예를 들어 식별자가 10인 Article에 대한 잠금을 구하고 싶다면 tryLock()을 실행할 때 ‘domain.Article’을 type 값으로 주고 ‘10’을 id 값으로 주면 된다.
•
tryLock()은 잠금을 식별할 때 사용할 LockId를 리턴한다. 이 책에서는 각 잠금마다 고유 식별자를 갖도록 구현되었다.
•
일단 잠금을 구하면 잠금을 해제하거나 잠금이 유효한지 검사하거나 잠금 유효 시간을 늘릴 때 LockId를 사용한다. LockId 클래스는 다음과 같다.
public class LockId {
private String value;
public LockId(String value) {
this.vlaue = value
}
public String getValue() {
return value;
}
}
Java
복사
•
오프라인 선점 잠금이 필요한 코드는 LockManager#tryLock()을 이용해서 잠금을 시도한다. 잠금에 성공하면 tryLock()은 LockId를 리턴한다.
•
이 LockId는 다음에 잠금을 해제할 때 사용한다. LockId가 없으면 잠금을 해제할 수 없으므로 LockId를 어딘가에 보관해야 한다.
•
다음은 컨트롤러가 오프라인 선점 기능을 이용해서 데이터 수정 폼에 동시에 접근하는 것을 제어하는 코드다. 수정 폼에서 데이터를 전송할 때 LockId를 전송할 수 있도록 LockId를 모델에 추가한다.
•
잠금을 선점하는 데 실패하면 LockException이 발생한다. 이때는 다른 사용자가 이미 접근 중이니 나중에 다시 시도하라는 안내 문구를 보여주면 된다.
•
수정 폼은 LockId를 다시 전송해서 잠금을 해제할 수 있도록 한다. 잠금을 해제하는 코드는 전달받은 LockId를 사용한다.
•
서비스 코드를 보면 LockManager#checkLcok() 메서드를 가장 실행하는데, 잠금이 선점된 이후에 실행하는 기능은 다음과 같은 상황을 고려하여 주어진 LockId를 갖는 잠금이 유효한지 확인해야 한다.
◦
잠금 유효 시간이 지났으면 이미 다른 사용자가 잠금을 선점한다.
◦
잠금을 선점하지 않은 사용자가 기능을 실행했다면 기능 실행을 방지해야 한다.
2. DB를 이용한 LockManager 구현
: DB를 이용한 LockManager를 구현해 보자. 잠금 정보를 저장할 테이블과 인덱스를 생성한다.
•
Order 타입의 1번 식별자를 갖는 애그리거트에 대한 잠금을 구하고 싶다면 다음과 같은 Insert 쿼리를 이용해 locks 테이블에 데이터를 삽입하면 된다.
◦
insert into locks values (’Order’, ‘1’, ‘생성한 lockId’, ‘2016-03-28 09:10:00’);
•
type과 id 칼럼을 주요키로 지정해서 동시에 두 사용자가 특정 타입 데이터에 대한 잠금을 구하는 것을 방지한다. 각 잠금마다 새로운 LockId를 사용하므로 lockid를 유니크 인덱스로 설정한다.
•
잠금 유효 시간을 보관하기 위해 expiration_time 칼럼을 사용한다.