JVM/JPA

낙관적 락과 비관적 락

kyoulho 2023. 7. 5. 16:53

JPA는 데이터베이스에 대한 동시 접근으로부터 엔티티에 대한 무결성을 유지할 수 있게 해주는 동시성 제어 메커니즘을 지원한다. 이 메커니즘에는 낙관적 락과 비관적 락이 존재한다. JPA는 데이터베이스의 트랜잭션 격리 레벨을 READ COMMITTED 정도로 가정한다.

 

낙관적 락 (Optimistic Lock)

대부분의 트랜잭션이 충돌이 발생하지 않을 것이라고 낙관적으로 가정하는 방법이다. 따라서 데이터베이스가 제공하는 락 기능을 사용하지 않고, 엔티티의 버전을 통해 동시성을 제어한다. 즉, 애플리케이션 레벨에서 지원하는 락이다.

 

@Version

JPA는 @Version 어노테이션을 제공하는데, 이를 사용하여 엔티티의 버전을 관리할 수 있다. @Version 적용이 가능한 타입은 Long(long), Integer(int), Short(short), Timestamp이다. @Version 은 아래와 같이 버전 관리용 필드를 만들어 적용한다.

@Entity
public class Board {

    @Id
    private String id;
    private String title;

    @Version
    private Integer version;
}

위 Board 엔티티가 변경될 때 마다 version 이 자동으로 하나씩 증가한다. 그리고 엔티티를 수정할 때, 엔티티를 조회한 시점의 버전과 수정한 시점의 버전이 일치하지 않으면 예외가 발생한다.

 즉, ‘최초 커밋만 인정하기’ 정책을 구현할 수 있다.

 

버전 정보 비교 방법

JPA가 엔티티를 수정하고 트랜잭션을 커밋하는 시점에, 영속성 컨텍스트를 flush 하면서 아래의 UPDATE 쿼리를 실행한다.

UPDATE BOARD
SET
   title = ?,
   version = ? 버전 + 1 증가
WHERE id = ?,
AND version = ? 버전 비교

위와 같이 데이터가 수정되었을 때, 엔티티의 버전 정보를 증가시킨다. 위 쿼리에서 WHERE 절에서 엔티티 조회 시점의 버전으로 데이터를 찾는 조건을 볼 수 있다. 만약 데이터 조회 이후 엔티티가 수정되었다면 위 WHERE 문으로 엔티티를 찾을 수 없다. 이 때 JPA가 예외를 던진다.

 

주의점

Embedded 타입의 경우 논리적으로 해당 엔티티의 값이므로 수정하면 엔티티의 버전이 증가한다. 반면 연관관계 필드의 경우 연관관계의 주인 필드를 변경할 때에만 버전이 증가한다.

또, @Version 으로 추가한 버전 관리 필드는 JPA가 직접 관리하므로 임의로 수정해서는 안된다. 그런데 벌크 연산의 경우 버전을 무시하므로, 벌크 연산을 수행할 때에는 아래와 같이 버전 필드를 강제로 증가시켜야 한다.

update Member m set m.name = '변경', m.version = m.version + 1

 

낙관적 락의 LockModeType

LockModeType을 통해서 락 옵션을 변경할 수 있다.

NONE

별도로 락 옵션을 지정하지 않아도 엔티티에 @Version을 적용하면 기본으로 적용되는 락 옵션이다.

  • 용도 : 조회한 엔티티를 수정하는 시점에 다른 트랜잭션으로부터 변경(또는 삭제)되지 않음을 보장한다. 즉, 조회 시점부터 수정 시점까지를 보장한다.
  • 동작 : 엔티티를 수정하는 시점에 엔티티의 버전을 증가시킨다. 이때 엔티티의 버전이 조회 시점과 다르다면 예외가 발생한다.
  • 이점 : 두 번의 갱신 분실 문제를 해결한다.

OPTIMISTIC

NONE의 경우 엔티티를 수정해야 버전을 체크하지만, 이 옵션은 엔티티를 조회만 해도 버전을 체크한다. 즉, 한번 조회한 엔티티가 트랜잭션 동안 변경되지 않음을 보장한다.

  • 용도 : 엔티티의 조회 시점부터 트랜잭션이 끝날 때까지 다른 트랜잭션에 의해 변경되지 않음을 보장한다.
  • 동작 : 트랜잭션을 커밋하는 시점에 버전정보를 체크한다.
  • 이점 : 애플리케이션 레벨에서 DIRTY READ와 NON-REPEATABLE READ를 방지한다.

OPTIMISTIC_FORCE_INCREMENT

낙관적 락을 사용하면서 버전 정보를 강제로 증가한다. 엔티티가 물리적으로 변경되지 않았지만, 논리적으로는 변경되었을 경우 버전을 증가하고 싶을 때 사용한다.

예를 들어 게시물과 첨부파일 엔티티가 1:N 관계로 있다고 가정하자. 게시물에 첨부파일이 하나 추가된 상황은 게시물 엔티티의 물리적 변경은 일어나지 않았지만, 논리적인 변경은 일어났다. 이때 버전을 변경하고 싶다면 해당 락 옵션을 사용하면 된다.

  • 용도 : 논리적인 단위의 엔티티 묶음을 관리할 수 있다.
  • 동작 : 엔티티가 직접적으로 수정되어 있지 않아도, 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해 버전 정보를 강제로 증가시킨다. 이때 엔티티의 버전을 체크하고 일치하지 않으면 예외가 발생한다. 이때 추가로 엔티티의 정보도 실제로 변경되었다면 2번의 버전 증가가 발생한다.
  • 이점 : 강제로 버전을 변경하여 논리적인 단위의 엔티티 묶음을 버전관리할 수 있다.

 

비관적 락 (Pessimistic Lock)

비관적 락은 실제로 데이터베이스의 락을 사용하여 동시성을 제어하는 방법이다. 주로 쿼리에 SELECT... FOR UPDATE 구문을 사용하고, 버전 정보는 사용하지 않는다. 락을 직접 걸기 때문에 아래의 두 가지 특징이 있다.

  1. 엔티티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있다.
  2. 데이터를 수정하는 즉시 트랜잭션의 충돌을 감지할 수 있다.

 

비관적 락의 LockModeType

PESSIMISTIC_WRITE

비관적 락이라고 하면 일반적으로 해당 옵션을 의미한다.

  • 용도/동작 : 데이터베이스에 SELECT ... FOR UPDATE를 사용하여 배타 락을 건다.
  • 이점 : NON-REPEATABLE READ를 방지한다.

PESSIMISTIC_READ

데이터를 반복 읽기만 하고 수정하지 않을 때 사용한다. 일반적으로 잘 사용하지 않는다고 한다. 데이터베이스 대부분은 방언에 의해 PESSIMISTIC_WRITE로 동작한다.

  • 동작 : SELECT... FOR SHARE (LOCK IN SHARE MODE)

PESSIMISTIC_FORCE_INCREMENT

비관적 락 중 유일하게 버전 정보를 사용한다. 비관적 락이지만 버전 정보를 강제적으로 증가시킨다. 하이버네이트의 경우 nowait를 지원하는 데이터베이스에 대해서 FOR UPDATE NOWAIT 옵션을 적용하고, 그렇지 않다면 FOR UPDATE를 적용한다.

 

동시성 제어 메커니즘과 트랜잭션 격리 수준의 차이점

JPA의 동시성 제어 메커니즘은 특정 엔티티에 대한 동시 접근을 막기 위해 사용한다. 반면, 트랜잭션 격리 수준은 트랜잭션 동안의 일관된 데이터 읽기를 고려하기 위해 적용한다.

DBMS에 따라 격리 레벨에 대한 세부 구현은 다르겠지만,  대부분의 데이터베이스는 트랜잭션 격리 레벨을 구현할 때 락을 사용하지 않는다고 한다.  특별한 예외로는 SERIALIZABLE은 조회 중인 데이터를 다른 트랜잭션이 변경하려고 할 때 락을 획득한다.

반면 낙관락/비관락은 그 관심사가 엔티티에 대한 동시 접근에 대한 처리이다. 한 트랜잭션이 특정 엔티티에 접근하고 있을 때 다른 트랜잭션이 해당 엔티티를 변경할 수 없도록 버전을 사용하거나 락을 걸어 해결한다. 즉, 트랜잭션 격리 수준과는 관계가 없다.

'JVM > JPA' 카테고리의 다른 글

PostgreSQL Array 타입 JPA 연동  (0) 2023.11.12
2차 캐시  (0) 2023.07.05
트랜잭션을 지원하는 쓰기 지연  (0) 2023.07.04
SQL 쿼리 힌트 사용  (0) 2023.07.04
읽기 전용 쿼리의 성능 최적화  (0) 2023.07.04