JPA는 데이터베이스 트랜잭션 격리 수준을 READ COMMITTED 정도로 가정한다.
만약 일부 로직에 더 높은 격리 수준이 필요하면 낙관적 락과 비관적 락 중 하나를 사용하면 된다.
추가적으로 네임드 락에 대해서도 다시 정리해보겠다.
낙관적 락 (OptimisticLock)
낙관적 락은 이름 그대로 트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 말이다.
DB가 제공하는 락 기능을 사용하는 것이 아니라 JPA가 제공하는 버전 관리 기능을 사용한다.
즉 애플리케이션이 제공하는 락이다.
낙관적 락은 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다는 특징이 있다.
충돌이 빈번하지 않을 때 사용하는 것이 좋다.
충돌이 빈번하다면 비관적 락을 사용하는 것이 성능상 이점이 있다.
실패했을 때의 재시도 로직를 개발자가 코드로 작성해야 한다.
@Version 애노테이션을 사용해서 구현한다.
버전은 엔티티의 값을 변경하면 자동으로 증가한다.
간단하게 코드를 살펴보자. 아래는 엔티티 클래스다.
@Entity
public class Stock {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//...기타 코드
@Version
private Long version;
}
다음은 엔티티의 JpaRepository 인터페이스다.
public interface StockRepository extends JpaRepository<Stock, Long> {
//버전을 활용.
@Lock(value = LockModeType.OPTIMISTIC)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithOptimisticLock(Long id);
}
낙관적 락의 시나리오는 다음과 같다.
- 사용자 1과 2가 존재하는데 동시에 데이터에 접근한다. 버전은 1이다.
- 사용자 1이 데이터를 변경하고 변경한 데이터를 커밋하는데, 여기서 버전이 2로 올라간다.
- 사용자 2가 마찬가지로 데이터를 변경하고 변경한 데이터를 커밋하는데, 현재 데이터의 버전이 2이다.
- 이러면 사용자 2가 처음에 가져간 데이터 버전 1과 다르기 때문에 데이터를 변경할 수 없다.
- 그러면 개발자가 작성한 재시도 로직을 타고 다시 데이터 변경을 시도한다.
비관적 락 (PessimisticLock)
이름 그대로 트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 걸고 보는 방법이다.
DB가 제공하는 락 기능을 사용한다. 대표적으로 select for update 구문이 있다.
JPA도 select for update를 사용한다.
충돌이 빈번하다면 낙관적 락보다는 비관적 락을 선택해야 한다.
다음은 JpaRepository를 사용해서 비관적 락을 구현한 코드다.
public interface StockRepository extends JpaRepository<Stock, Long> {
//Lock을 걸고 데이터를 가져온다.
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithPessimisticLock(Long id);
}
네임드 락(NamedLock)
네임드 락은 GET_LOCK() 함수를 이용해 임의의 문자열에 대해 잠금을 설정할 수 있다.
네임드 락은 단순히 사용자가 지정한 문자열에 대해 획득하고 반납하는 잠금이다.
네임드 락은 자주 사용되지 않으며, 보통 분산 락에서 사용한다.
예를 들어, 데이터베이스 서버 1대에 5대의 웹 서버가 접속해서 서비스하는 상황에서 5대의 웹 서버가 어떤 정보를 동기화해야 하는 요건처럼 여러 클라이언트가 상호 동기화를 처리해야 할 때 네임드 락을 이용하면 쉽게 해결할 수 있다.
웹 애플리케이션에서 네임드 락을 사용하면 네임드 락 DataSource를 분리해야 한다.
커넥션의 수가 부족해질 수 있기 때문이다.
다음은 native query를 사용해서 구현한 JpaRepository 예시 인터페이스다.
public interface LockRepository extends JpaRepository<Stock, Long> {
@Query(value = "select get_lock(:key, 3000)", nativeQuery = true)
void getLock(String key);
@Query(value = "select release_lock(:key)", nativeQuery = true)
void releaseLock(String key);
}
참고 자료
자바 ORM 표준 JPA 프로그래밍(김영한)
댓글