본문 바로가기
프로그래밍

14. [JPA] Lock - 잠금

by 탁구치는 개발자 2017. 9. 20.

JPA에는 두 가지의 Lock을 제공한다.


[낙관적락]

트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법이다.

이것은 데이터베이스가 제공하는 락 기능을 사용하는것이 아니라 JPA가 제공하는 버전 관리 기능을 사용한다.

애플리케이션이 제공하는 락이다. 

트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다.


[비관적락]

트랜잭션의 충돌이 발생한다고 가정하고 우선적으로 락을 걸고 보는 방법이다.

이것은 데이터베이스가 제공하는 락기능을 사용한다.

대표적으로 select for update 구문이 있다.


JPA에서는 @Version 애노테이션을 통해 낙곽적락을 제공한다.

1
2
@Version
private int version;
cs

테이블에 VERSION이라는 컬럼을 생성하고 엔티티 매핑 시 @Version 애노테이션을 붙이면 엔티티에 대한 버전 관리를 제공한다.

버전은 최초 엔티티 생성시 0으로 셋팅 되며 이후 변경이 있을 때마다 1씩 증가한다.


그럼 어떻게 낙관적락을 제공하는지 sequence flow를 보자.

1. A트랜잭션이 emp테이블의 emp_no = 1 값을 가져온다. 이때 version은 0이다.
2. B트랜잭션이 emp테이블의 emp_no = 1 값을 가져온다. 이때 version은 0이다.
3. A트랜잭션이 조회한 엔티티를 변경하여 커밋한다.
 > 커밋하기 전에 JPA에서는 version을 확인해 본다. 현재 엔티티의 version과 DB의 version 값이 같은지
 > 엔티티 버전와 DB 버전이 같다면 emp 테이블의 version 컬럼에 1로 업데이트 한다.
4. B트랜잭션이 조회한 엔티티를 변경하여 커밋한다. 
 > 커밋하기 전에 version을 확인한다. 어라~ 근데 내가 조회한 엔티티 version은 0인데 DB의 version은 1로 되어 있다.
 > 예외를 발생시킨다
cs


테스트를 해보자.

엔티티 클래스를 생성하고 version 속성에 @Version 애노테이션을 추가하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
@Table(name = "OPTIMISTIC_MEMBER")
@Entity
class OptimisticMember {
 
    @Id
    private int id;
 
    @Column(name = "NAME")
    private String name;
 
    @Version
    @Column(name = "VERSION")
    private int version;
}
cs


별도의 트랜잭션 생성을 위해서 MemberService 생성하였다.

Transactional.TxType.REQUIRES_NEW 추가하면 새로운 트랜잭션을 생성하겠다는 의미이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
class MemberService {
 
    @Getter
    @PersistenceContext
    private EntityManager em;
 
    @Transactional(Transactional.TxType.REQUIRES_NEW)
    public void updateMember(OptimisticMember member) {
        em.merge(member);
        em.flush();
    }
}
cs


예외 발생 테스트

예상한대로 OptimisticLockException 예외가 발생됨을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test(expected = OptimisticLockException.class)
@Transactional
public void 낙관적락예외발생() {
    // 영속 상태의 OptimisticMember
    OptimisticMember member = em.find(OptimisticMember.class1); // version 0
 
    // version 1로 변경되어 DB 반영됨
    member.setName("nklee2");
    memberService.updateMember(member);
 
    try {
        // update 시 version이 0이므로 낙관적 락 오류 발생 (DB는 1로 되어 있음)
        member.setName("nklee3");
        memberService.updateMember(member);
    } catch (Exception e) {
        e.printStackTrace(); // javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)
        throw e;
    }
 
    fail("Optimistic lock exception 발생!!");
}
cs



비관적락은 쿼리할 때 우선적으로 락을 거는 기법이다.

엔티티를 가져올 때 다음처럼 사용하면 된다.

1
em.find(PessimisticMember.class1, LockModeType.PESSIMISTIC_WRITE);
cs

쿼리 로그를 확인해 보면 데이터베이스에서 제공하는 락 기능인 select for update 구문이 추가된 것을 확인할 수 있다.

1
2
3
4
5
6
7
select
    pessimisti0_.id as id1_46_0_,
    pessimisti0_.name as name2_46_0_ 
from
    pessimistic_member pessimisti0_ 
where
    pessimisti0_.id=? for update
cs


테스트 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Test(expected = PessimisticLockingFailureException.class)
@Transactional
public void 비관적락_예외_CASE_1() {
    em.find(PessimisticMember.class1, LockModeType.PESSIMISTIC_WRITE); // where pessimisti0_.id=? for update (H2)
    em.flush();
 
    // 해당 row에 lock이 설정되어 있어 오류 발생
    memberService.updateMember(1);
 
    fail("Timeout trying to lock table ; SQL statement:");
}
 
@Service
class PMemberService {
 
    @Getter
    @PersistenceContext
    private EntityManager em;
 
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateMember(int id) {
        em.setProperty("javax.persistence.query.timeout"10000);
        PessimisticMember member = em.find(PessimisticMember.class, id);
        member.setName("Lee namkyu");
    }
}
 
@Data
@Table(name = "PESSIMISTIC_MEMBER")
@Entity
class PessimisticMember {
 
    @Id
    private int id;
 
    @Column(name = "NAME")
    private String name;
}
cs