동시 접근 제어

Hibernate 2008.03.29 00:28
프로젝트 인해 오랜만에 씁니다.

하나의 영속 객체에 두명이 작업을 한다면 어떤 방법으로 처리해야 할까요?
하이버네이트에서는 낙관적 잠금, 비관적 잠금 이라는 두가지 방법으로 처리 합니다.

낙관적 잠금

사용자 삽입 이미지
사용자1이 먼저 데이터를 가져온 후 사용자2가 데이터를 가져오지만 늦게 가져온 사용자2가 먼저 update를 수행합니다. 그리고 사용자1이 뒤늦게 udpate를 수행 하지만 수정에 실패하고 예외가 발생 합니다.
먼저 수정한 쪽에 우선권을 주는 것 이게 바로 낙관적 잠금의 개념 입니다.

하이버네이트 매핑 설정에서 <class /> 요소에 보면 optimistic-lock 이라는 속성이 있습니다.
이 속성이 바로 낙관적 잠금에 대한 속성인데 디폴트 값은 "version" 입니다. version 이라는 것은 말 그대로 객체의 버전을 부여한다는 의미 입니다. version을 이용한 낙관적 잠금의 설정은 <version /> 요소를 이용하는데 버전 프로퍼티가 별도로 필요 합니다. 위 테스트 코드에서 첫번째 트랜잭션이 컴밋하는 순간 버전 컬럼의 값이 1 증가하게 됩니다.
그리고 두번째 트랜잭션이 컴밋할 때 이미 버전이 증가 됐으므로 update를 수행 했을때 수정된 레코드 수는 0 입니다. update의 결과가 0이기 때문에 StaleObjectStateException 예외를 던지는 것 입니다. 이 예외를 잘 처리해서 사용자2에게 적절한 메세지를 보여주면 되는 것입니다.

만약 버전 컬럼을 추가할 수 없을 경우에는...?

버전 컬럼을 추가 할 수 없을경우 <class /> 요소의 optimistic-lock 속성의 값을 변경하면 됩니다.
설정 가능한 나머지 값은 "none", "all", "dirty" 입니다.
none은 낙관적 잠금을 사용하지 않고
all은 모든 컬럼을 비교해서 낙관적 잠금을 하며
dirty는 변경된 컬럼만 비교해서 낙관적 잠금을 합니다.
all과 dirty의 경우 update시 변경된 컬럼만 수정하기 때문에 dynamic-update 속성을 true 설정해야 합니다.

unsaved-value 속성에 대해서...
<version /> 요소에 있는 속성으로서 역할은 객체가 새로운 객체인지 저장되었던 객체인지 구별하게 해줍니다. 지정된 설정 값이 "null" 일 경우 저장되지 않은 객체의 버전 프로퍼티의 값이 null 이라는 것을 뜻하고 "negative" 일 경우 저장되지 않은 객체의 버전 프로퍼티 값이 [음수] 라는 것을 뜻합니다. 그리고 "undefined" 는 버전 프로퍼티 대신 id 프로퍼티를 통해 상태를 구별하게 됩니다.
그런데 이 속성은 하이버네이트3 에서 더이상 필요 없다고 래퍼런스에 나와 있습니다. 일단 새로운 객체일 경우 id 프로퍼티를 통해 객체의 상태를 알 수 있고 나머지 상태의 객체는 버전 프로퍼티가 결코 null이거나 기본형일 경우 0 이하가 될 수 없기 때문 입니다.

<timestamp />는 비추...
<timestamp />는 <version type="timestamp" />와 같고
<timestamp use-db="ture" />는 <version type="dbtimestamp" /> 와 같습니다.
비추 되는 이유는 기선님 블로그찬욱님 블로그에 vm에서 받아오는 시간의 경우 클러스터링 환경에서 안정성을 보장받지 못하며 db에서 받아오는 시간의 경우 db를 다녀오는 비용이 발생하기 때문이라고 나와 있습니다.




비관적 잠금
사용자 삽입 이미지
사용자1이 먼저 데이터를 가져오는데 데이터를 가져오는 시점에서 락을 걸어놓게 되는데 이 때 사용자2가 데이터에 접근하면 사용자1이 작업을 종료할 때까지 데이터에 접근 할 수 가 없습니다. 이런 방법을 비관적 잠금 이라고 부릅니다.

비관적 잠금은 get(), load() 메서드로 객체를 가져올 때 세번째 인자로
LockMode.UPGRADE, LockMode.UPGRADE_NOWAIT 값을 지정 하면 됩니다.
UPGRADE의 경우 SELECT ... FOR UPDATE 쿼리가 실행되며
UPGRADE_NOWAIT의 경우 SELECT ... FOR UPDATE NOWAIT 쿼리가 실행 됩니다.
따라서 비관적 잠금은 DBMS에서 위의 쿼리를 지원해야 사용할 수 있습니다.

session.get(..., 1, LockMode.UPGRADE);
session2.get(..., 1, LockMode.UPGRADE);
두번째 세션에서 SELECT ... FOR UPDATE 쿼리를 수행하지만 락이 걸렸으므로 블럭킹 상태에 이르게 됩니다.

session.get(..., 1, LockMode.UPGRADE_NOWAIT);
session2.get(..., 1, LockMode.UPGRADE_NOWAIT);
이번에는 두번째 세션이 블럭킹 상태가 되지 않고 바로 예외가 던져 집니다.

session.get(..., 1);
session.lock(..., LockMode.UPGRADE);
객체를 가져온 이후에 락을 걸 수도 있습니다. 하지만 쿼리가 두번 실행되므로 get(), load() 메서드 호출시 락을 거는게 좋습니다.




LockMode와 lock() 메서드...
6가지 락 모드
LockMode.NONE : 락을 사용하지 않습니다. get(), load() 메서드의 세번째 인자인 LockMode 를 생략하는 것과 동일 합니다.
           
LockMode.READ : 현재 자바 객체와 DB 객체와의 버전을 비교 합니다. 만약 버전이 다르다면 즉시 예외가 던져 집니다.
           
LockMode.UPGRADE : 락을 겁니다. 만약 이미 락이 걸려 있다면 락이 풀릴 때까지 블럭킹 상태가 됩니다. DBMS에서 SELECT ... FOR UPDATE 쿼리를 지원하지 않는다면 READ 로 됩니다.
           
LockMode.UPGRADE_NOWAIT : 락을 겁니다. UPGRADE 와 다르게 이미 락이 걸려 있다면 예외를 던집니다. DBMS에서 SELECT ... FOR UPDATE NOWAIT 쿼리를 지원하지 않는다면 UPGRADE 로 됩니다.

LockMode.FORCE : 버전 값을 강제로(변경 사항이 없더라도) 증가 시킵니다.
          
LockMode.WRITE : 하이버네이트가 한 행을 insert, update 할 때 자동으로 획득 된다는데 이게 정확히 무슨 의미인지 모르겠네요... 아시는분 계시면 알려주세요.

lock() 메서드의 용도
- 영속 객체에 대한 지정된 수준의 잠금을 구합니다.
- 준영속 객체를 재첨부 합니다.
UPGRADE, UPGRADE_NOWAIT의 경우 궂이 객체를 가져온 후에 lock() 메서드를 호출할 필요는 없을 것 같습니다. get(), load() 메서드로 객체를 가져오는 타임에 애초에 LockMode를 인자로 주는것이 더 직관적 이고 쿼리도 절약 됩니다. 제가 생각하는 용도는 딱 두가지 입니다. FORCE와 READ인데 다음 예를 들어 보겠습니다.

FORCE의 경우
article.getArticleDetail().setContent("내용 수정");
위 코드에서 article 객체가 아닌 연관 객체가 변경 되었습니다. flush 될 경우 ArticleDetail 객체의 버전은 증가 되지만 root 객체인 Article의 버전은 증가 되지 않습니다. 이런 상황에서 lock(article, LockMode.FORCE); 를 호출한다면 Article 객체의 변경이 없더라도 버전은 강제로 증가 됩니다.

READ의 경우
session.lock(article, LockMode.READ);   // article은 준영속 객체
준영속 객체를 재첨부 하는 시점에서 DB 객체와의 버전을 체크하기 위해 사용 합니다. lock() 메서드 호출 시점에서 즉시 버전체크를 하며 버전이 다르면 예외가 던져 집니다. lock() 메서드로 객체를 재첨부 할 때 주의 사항이 있는데 lock() 메서드를 호출하기 전에는 객체에 어떠한 변경도 이루어지면 안된다는 것 입니다.
article.setTitle("제목 미리 변경");            // 준영속 상태에서 값을 미리 변경
session.lock(article, LockMode.READ);
article.setTitle("제목을 다시 변경");         // 재첨부 이후에 다시한번 값을 변경
만약 재첨부 이전에 값을 미리 변경 했다면 해당 프로퍼티는 재첨부 이후에 값을 변경하더라도 dirty 체킹에서 제외 됩니다. 래퍼런스에도 lock() 메서드로 재첨부시 준영속 객체는 변경되지 않아야 한다고 나와 있습니다.

신고

'Hibernate' 카테고리의 다른 글

HQL - API  (0) 2008.04.13
늦은로딩과 조회방식  (0) 2008.04.09
동시 접근 제어  (0) 2008.03.29
openSession()과 currentSession()의 close()  (0) 2008.03.04
Session으로 작업하기  (0) 2008.03.02
객체의 상태  (0) 2008.03.02
Posted by 째코