====== Concurrency ======
동시에 동일한 데이터에 접근할 때에 데이터에 대한 접근을 제어하기 위해 Optimistic Locking을 지원한다. 한편 Hibernate의 Native API를 통해서는 지원 가능한 Pessimistic Locking 은 JPA2.0 버전에 정의될 예정이다.
===== Optimistic Locking =====
=== Without Locking Source ===
@Test
public void testUpdateUserWithoutOptimisticLocking() throws Exception {
// 1. 테스트를 위한 신규 데이터를 입력
newTransaction();
addDepartmentUserAtOnce();
closeTransaction();
// 2. 동일한 식별자를 이용하여 User 정보를 두번 조회
newTransaction();
User fstUser = (User) em.find(User.class,"User1");
User scdUser = (User) em.find(User.class,"User1");
closeTransaction();
// 3. Detached 상태에서의 변경처리
fstUser.setUserName("First : Kim");
// 4. 별도의 트랜잭션으로 변경처리
newTransaction();
scdUser.setUserName("Second : Kim");
closeTransaction();
// 5. 3에서 작업한 내용이 반영되어 변경.
newTransaction();
em.merge(fstUser);
closeTransaction();
}
위에서 제시한 로직에 대해 자세히 살펴보자.
- #1, #2번 코드에 의해 각각 동일한 식별자를 이용하여 같은 데이터 조회
- 두번째 트랜잭션이 종료된 후, #3번 코드에서는 Detached 상태의 fstUser 객체의 userName 변경
- 세번째 트랜잭션 내의 #4번 코드에서는 scdUser 객체의 userName 변경, 세번째 트랜잭션 종료시 변경 사항이 DB에 반영
- 네번째 트랜잭션 내에서 #3번 코드를 통해 변경된 fstUser 객체에 대해 update 수행
- fstUser에 대한 수정 작업 또한 성공적으로 처리
결론적으로 보면, userId가 "User1"인 User의 userName은 "First : Kim"이 되어 앞서 scdUser에서 요청했던 수정 작업은 무시된 것이다.
이러한 현상을 Lost Update라고 하며, 이를 해결하기 위한 방법은 3가지가 있다.
- Last Commit Wins : Optimistic Locking 을 수행하지 않게 되면 기본적으로 수행되는 유형으로 2개의 트랜잭션 모두 성공적으로 commit된다. 그러므로 두번째 commit은 첫번째 commit 내용을 덮어쓸 수 있다. (위의 예의 경우)
- First Commit Wins : Optimistic Locking을 적용한 유형으로 첫번째 commit만이 성공적으로 이루어지며, 두번째 commit 시에는 Error를 얻게 된다.
- Merge : 첫번째 commit만이 성공적으로 이루어지며, 두번째 commit 시에는 Error를 얻게 된다. 그러나 First Commit Wins와는 달리 두번째 commit을 위한 작업을 처음부터 다시 하지 않고 개발자의 선택에 의해 선택적으로 변경될 수 있도록 한다. 가장 좋은 전략이나 변경 사항을 merge 할 수 있는 화면이나 방법을 직접 제공해 줄 수 있어야 한다.(추가 구현 필요함)
JPA에서는 Versioning 기반의 Automatic Optimistic Locking을 통해 First Commit Wins 전략을 취할 수 있도록 지원한다. JPA에서 Optimistic Locking을 수행하기 위해서는 해당 테이블에 Version을 추가해야 한다. 그러한 경우 해당 테이블과 매핑된 객체를 로드할 때 Version 정보도 함께 로드되고 객체 수정시 테이블의 현재 값과 비교하여 처리 여부를 결정하게 된다.
=== With Optimistic Locking Source ===
@Test
public void testUpdateDepartmentWithOptimisticLocking() throws Exception {
// 1. 테스트를 위한 신규 데이터를 입력
newTransaction();
addDepartmentUserAtOnce();
closeTransaction();
// 2. Department 정보를 두번 조회
newTransaction();
Department fstDepartment = (Department) em.find(Department.class,"Dept1");
assertEquals("fail to check a version of department.", 0, fstDepartment.getVersion());
Department scdDepartment = (Department) em.find(Department.class,"Dept1");
closeTransaction();
// 3. 두번째 조회한 Department 정보에 다른 deptName을 셋팅하여 DB에 반영
fstDepartment.setDeptName("First : Dept.");
// 4. 첫번째 조회한 Department 정보에 대해 merge() 메소드를 호출
newTransaction();
scdDepartment.setDeptName("Second : Dept.");
closeTransaction();
// 5. 세번째 트랜잭션에서의 수정으로 인해 DEPARTMENT_VERSION이 이미 변경되었기 때문에
// StaleObjectStateException 발생이 예상
newTransaction();
try {
em.merge(fstDepartment);
closeTransaction();
} catch (Exception e) {
e.printStackTrace();
assertTrue("fail to throw StaleObjectStateException.",e instanceof StaleObjectStateException);
}
}
위와같이 다음의 testUpdateDepartmentWithOptimisticLocking() 메소드를 수행하였을 때 첫번째 수정 작업은 성공적으로 이루어지나 두번째 수정 작업에 대해서는 #6번 코드에서처럼 StaleObjectStateException이 throw될 것이다. 이를 위한 entity 클래스의 설정의 일부분은 다음과 같다.
=== Entity Class Source ===
@Entity
@Table(name="DEPARTMENT")
public class Department {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "DEPT_ID", length = 10)
private String deptId;
@Version
@Column(name = "DEPT_VERSION")
private int version;
...
}
위에서 보는 것 같이 DEPT_VERSION이라는 컬럼을 추가하여 버전관리를 하게 함으로써 Optimistic Locking처리를 할 수 있다.