/////
Search
Duplicate
4️⃣

오프라인 선점 잠금

: 아틀라시안의 컨플루언스는 문서를 편집할 때 누군가 먼저 편집을 진행하고 있다면 다른 사용자가 문서를 수정하고 있다는 안내 문구를 보여준다.
: 이런 안내를 통해 여러 사용자가 동시에 한 문서를 수정할 때 발생하는 충돌을 사전에 방지할 수 있게 해준다.
: 컨플루언스는 사전에 충돌 여부를 알려주지만 동시에 수정하는 것을 막진 않는다. 더 엄격하게 데이터 충돌을 막고 싶다면 누군가 수정 화면을 보고 있을 때 수정화면 자체를 실행하지 못하도록 해야 한다.
: 한 트랜잭션 범위에서만 적용되는 선점 잠금 방식이나 나중에 버전 충돌을 확인하는 비선점 잠금 방식으로는 이를 구현할 수 없다. 이때 필요한 것이 바로 오프라인 선점 잠금 방식이다.
: 단일 트랜잭션 동시 변경을 막는 선점 잠금 방식과 달리 오프라인 선점 잠금은 여러 트랜잭션에 걸쳐 동시 변경을 막는다.
: 첫 번째 트랜잭션을 시작할 때 오프라인 잠금을 선점하고 마지막 트랜잭션에서 잠금을 해제한다. 잠금을 해제하기 전까지 다른 사용자는 잠금을 구할 수 없다.
: 예를 들어 수정 기능을 생각해 보자, 보통 수정 기능은 두 개의 트랜잭션으로 구성된다.
첫 번째 트랜잭션은 폼을 보여주고 두 번재 트랜잭션은 데이터를 수정한다.
오프라인 선점 잠금을 사용하면 과정 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 칼럼을 사용한다.