JPA에서 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock) 사용
이 전 블로그 글에는 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)이 무엇인지 알아보았다.
이제 JPA에서 낙관적 락과 비관적 락을 어떻게 사용하는지 알아보자.
JPA 락 사용(Lock)
락은 다음 위치에 적용할 수 있다.
- EntityManager.lock(), EntityManager.find(), EntityManager.refresh()
- Query.setLockMode() (TypeQuery 포함)
- @NamedQuery
// 조회 시 Locking
Board board = em.find(Board.class, id, LockModeType.OPTIMISTIC);
// 조회 후 필요할 때 Locking
Board board = em.find(Board.class, id);
em.lock(board, LockModeType.OPTIMISTIC);
LockModeType의 타입은 다음과 같다.
JPA를 사용할 때 추천하는 전략은 두 번의 갱신 내역 분실 문제의 예방을 위해 READ COMMITTED + 버전 관리(낙관적 락)다.
JPA 낙관적 락(Optimistic Lock)
JPA가 제공하는 낙관적 락을 사용하려면 @Version 어노테이션을 사용해서 버전 관리 기능을 추가해야 한다.
@Version은 Long(long), Integer(int), Short(short), Timestamp 타입 모두 적용이 가능하다.
@Entity
public class Board {
...
@Version
private Integer version;
}
엔티티에 버전 관리용 필드를 추가하고 @Version을 붙이면 된다.
엔티티의 값을 변경하면 버전이 하나씩 자동으로 증가한다. 그리고 엔티티를 수정할 때 조회 시점의 버전과 수정 시점의 버전이 다르면 예외가 발생한다.
트랜잭션1와 트랜잭션2가 동시에 조회 후 트랜잭션2가 먼저 데이터를 수정하고 트랜잭션1가 수정했을때
트랜잭션1과 트랜잭션2의 Version이 다르므로 예외가 발생한다. 따라서 버전 정보를 사용하면 최초 커밋만 인정하기가 적용된다.
단, 연관관계 필드는 외래 키를 관리하는 연관관계의 주인 필드를 수정할 때만 버전이 증가한다.
@Version으로 추가한 버전 관리 필드는 JPA가 직접 관리하므로 벌크 연산을 제외하곤 개발자가 임의로 수정하면 안 된다. 벌크 연산은 버전을 무시하기 때문에 벌크 연산에서 버전을 증가하려면 버전 필드를 강제로 증가시켜야 한다.
낙관적 락은 트랜잭션을 커밋하는 시점에 충돌을 할 수 있다는 특징이 있다.
낙관적 락에서 발생하는 예외는 다음과 같다.
- javax.persistence.OptimisticLockException (JPA 예외)
- org.hibernate.StaleObejctStateException (하이버네이트 예외)
- org.springframework.orm.ObjectOptimisticLockingFailureException (스프링 예외 추상화)
참고로 락 옵션 없이 @Version만 있어도 낙관적 락이 적용된다.
락 옵션을 사용하면 락을 더 세밀하게 제어할 수 있다.
NONE
락 옵션을 적용하지 않아도 엔티티에 @Version이 적용된 필드만있으면 낙관적 락이 적용된다.
- 용도 : 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경(삭제)되지 않아야 한다. 조회 시점부터 수정 시점까지를 보장한다.
- 동작 : 엔티티를 수정할 때 버전을 체크하면서 버전을 증가한다.(UPDATE 쿼리 사용) 이때 데이터베이스의 버전 값이 현재 버전이 아니면 예외가 발생한다.
- 이점 : 두 번의 갱신 분실 문제(Second lost updates problem)를 예방한다.
OPTIMISTIC
@Version만 적용했을 때는 엔티티를 수정해야 버전을 체크하지만 이 옵션을 추가하면 엔티티를 조회만 해도 버전을 체크한다. 쉽게 이야기해서 한 번 조회한 엔티티는 트랜잭션을 종료할 때까지 다른 트랜잭션에서 변경하지 않음을 보장한다.
- 용도 : 조회한 엔티티는 트랜잭션이 끝날 때까지 다른 트랜잭션에 의해 변경되지 않아야 한다. 조회 시점부터 트랜잭션이 끝날 때까지 조회한 엔티티가 변경되지 않음을 보장한다.
- 동작 : 트랜잭션을 커밋할 때 버전 정보를 조회해서(SELECT 쿼리 사용) 현재 엔티티의 버전과 같은지 검증한다. 만약 같지 않으면 예외가 발생한다.
- 이점 : OPTIMISTIC 옵션은 DIRTY READ와 NON-REPEATABLE READ를 방지한다.
OPTIMISTIC_FORCE_INCREMENT
낙관적 락을 사용하면서 버전 정보를 강제로 증가한다.
- 용도 : 논리적인 단위의 엔티티 묶음을 관리할 수 있다.
예를 들어 A엔티티에서 연관관계가 있는 B엔티티만 수정되었을때 A엔티티 버전도 증가 해야 하는데, 이때 버전 정보를 강제로 증가시킨다. - 동작 : 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때 버전 정보를 강제로 증가시킨다. 만약 B엔티티에 이어 A엔티티도 수정되었을때는 2번에 버전 증가가 나타날 수 있다.
- 이점 : 강제로 버전을 증가해서 논리적인 단위의 엔티티 묶음을 버전 관리할 수 있다.
OPTIMISTIC_FORCE_INCREMENT는 DDD에 Aggregate Root에 사용할 수 있다.
DDD 카테고리의 애그리거트 게시 글을 확인하면 도움이 될 수 있다.
JPA 비관적 락(Pessimistic Lock)
JPA가 제공하는 비관적 락은 데이터베이스 트랜잭션 락 메커니즘에 의존하는 방법이다. 주로 SQL 쿼리에 select for update 구문을 사용하면서 시작하고 버전 정보는 사용하지 않는다.
비관적 락은 주로 PESSIMISTIC_WRITE 모드를 사용한다.
비관적 락은 다음과 같은 특징이 있다.
- 엔티티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있다.
- 데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다.
비관적 락에서 발생하는 예외는 다음과 같다.
- javax.persistence.PessimisticLockException (JPA 예외)
- org.springframework.dao.PessimisticLockingFailureException (스프링 예외 추상화)
PESSIMISTIC_WRITE
비관적 락이라 하면 일반적으로 이 옵션을 뜻한다. 데이터베이스에 쓰기 락을 걸때 사용한다.
- 용도 : 데이터베이스에 쓰기 락을 건다.
- 동작 : 데이터베이스 select for update를 사용해서 락을 건다
- 이점 : NON-REPREATABLE READ를 방지한다. 락이 걸린 Row는 다른 트랜잭션이 수정할 수 없다.
PESSIMISTIC_READ
데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용한다. 일반적으로 잘 사용하지 않는다.
데이터베이스 대부분은 방언에 의해 PERSSIMISTIC_WRITE로 동작한다.
- MySQL : lock in share mode
- PostgreSQL : for share
PESSIMISTIC_FORCE_INCREMENT
비관적 락 중 유일하게 버전 정보를 사용한다. 비관적 락이지만 버전 정보를 강제로 증가시킨다.
하이버네이트는 nowait를 지원하는 데이터베이스에 대해서 for update nowait 옵션을 적용한다.
- 오라클 : for update nowait
- PostgreSQL : for update nowait
- nowait를 지원하지 않으면 for update가 사용된다.
비관적 락과 타임아웃
비관적 락을 사용하면 락을 획득할 때까지 트랜잭션이 대기하는데 무한정 기다릴 수 없으므로 타임아웃 시간을 줄 수 있다.
Map<String, Object> properties = new HashMap<String, Object>();
// 타임아웃 10초까지 대기 설정
properties.put("javax.persistence.lock.timeout", 10000);
Board board = em.find(Board.class, "boardId", LockModeType.PESSIMISTIC_WRITE, properties);
타임아웃은 데이터베이스 특성에 따라 동작하지 않을 수 있다.