Home 19. JPA 잠금 기법
Post
Cancel

19. JPA 잠금 기법

01. 동시 접근과 잠금

[동시 접근을 올바르게 처리하지 않을 대 발생하는 데이터 불일치 문제]

위 그림을 보면 인출과 입금 이후에 실제 잔고는 800원이어야 하는데 남은 잔고는 500원이 된다.

이렇게 동시성 접근 문제를 막기 위한 가장 쉬운 방법은 DB 트랜잭션 격리 수준을 높이는 것이다. 단, 트랜잭션 격리 수준을 높이면 동시 사용자가 많은 온라인 서비스에서는 전체 성능을 떨어뜨리기 때문에 동시 접근을 처리하기 위한 올바른 방법이 아니다.

트랜잭션 격리 수준을 높이는 대신 동시 접근을 처리할 수 있는 다른 방법으로는 잠금 기법 을 사용하는 것이다. 잠금 기법에는 먼저 데이터에 접근한 트랜잭션이 우선순위를 갖는 선점 잠금 방식과 먼저 데이터를 수정한 트랜잭션이 우선순위를 갖는 비선점 잠금 방식이 존재한다.

02. 선점 잠금(pessimistic lock)

선점 잠금은 먼저 데이터에 접근한 트랜잭션이 우선순위를 갖는 잠금 방식이다.

선점 잠금을 사용하면 서로 다른 두 트랜잭션이 동시에 동일 데이터에 접근하여 수정하는 것을 방지할 수 있고, 동시 접근으로 데이터 일관성이 깨지는 것을 막아준다. JPA에서 선점 잠금을 사용하려면 find() 메서드의 세 번째 인자로 LockModeType.PESSIMISTIC_WRITE를 값으로 전달하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DepositService {
  public int deposit(String accountNum, int value) {
    EntityManager em = EMF.currentEntityManager();
    try {
      em.getTransaction().begin();
      //LockModeType.PESSIMISTIC_WRITE를 사용하면 하이버네이트는 DBMS의 잠금 쿼리를 사용해서 행 단위 잠금을 사용한다.
      Account account = em.find(Account.class, accountNum, LockModeType.PESSIMISTIC_WRITE);
      if(account == null) {
        throw new AccountNotFoundException();
      }
      account.deposit(value);
      em.getTransacion().commit();

      return account.Balance();

    }catch(Exception ex) {
      em.getTransacion().rollback();
      throw ex;
    }finally {
      em.close();
    }
  }
}

한 트랜잭션이 식별자가 “02-001”인 Account 엔티티를 LockModeType.PESSIMISTIC_WRITE를 이용해서 구현했다면, 이 트랜잭션의 잠금이 해제될 때까지 “02-001” Account 엔티티에 대한 선점 잠금을 구하지 못하고 블록킹 된다.

선점 잠금을 사용할 때 주의사항으로는 교착 상태 에 빠질 수 있다는 것이다. 선점 잠금으로 인해 트랜잭션이 교착상태에 빠지는 상황을 방지하려면 짐금 대기 시간을 힌트로 설정하면 된다.

1
2
3
4
Map<String, Object> hints = new HashMap<>();
//DBMS에 따라 잠금 대기시간지 적용되지 않을 수 있다.
hints.put("javax.persistence.lock.timeout", 1000);
Account account = em.find(Account.class, accountNum, LockModeType.PESSIMISTIC_WRITE, hints);

03. 비선점 잠금(optimistic lock)

비선점 잠금 방식은 먼저 데이터를 수정한 트랜잭션이 우선순위를 갖는다.

비선점 잠금 방식을 사용하려면 버전 값을 저장할 컬럼이 필요하다. JPA는 엔티티를 조회할 때 버전 값을 함께 조회한다. 그리고 엔티티의 데이터가 바뀌면 update 쿼리의 비교 조건으로 식별자뿐만 아니라 데이터를 조회한 시점의 버전 값도 함께 사용한다. 그리고 update 쿼리에서 버전을 1 증가 시킨다. 만약 다른 트랜잭션에서 먼저 데이터를 수정하게 되면 버전 값이 일치하지 않아 데이터 수정에 실패한다.

[JPA에서 비선점 잠금을 사용하기 위한 방법]

  • 버전 값을 저장할 컬럼 추가(컬럼 타입은 숫자나 시간 타입)
  • 버전 컬럼과 매핑할 속성에 @Version 애노테이션을 설정
1
2
3
4
5
6
7
8
9
10
11
@Entity
public class Customer {
  @Id
  private String id;

  @Version
  private Integer ver;

  @Column(name="secret_code")
  private String secretCode;
}

@Version으로 설정할 수 있는 타입으로는 int, Integer, short, Short, long, Long, Timestamp이다.

1
2
3
4
5
6
7
8
em.getTransaction().begin();
//select c.id, c.secret_cpde, c.ver from Customer c where c.id=?
Customer customer = em.find(Customer.class, id);

customer.changeSecretCode(newSecCode);

//update Customer set secret_code=?, ver=?, where id=? and ver=?
em.getTransacion().commit();

만약 update 쿼리 실행 결과로 변경된 행의 개수가 0이면, 트랜잭션을 롤백하고 익셉션을 발생한다.

객체를 생성할 때 버전을 지정하지 않아 ver가 null이면 하이버네이트는 버전 값으로 0을 사용한다.

This post is licensed under CC BY 4.0 by the author.