Optimistic Lock, Pessimistic Lock

code4109·2022년 11월 24일
0

JPA

목록 보기
2/2

낙관적 락과 비관적 락

  • JPA는 DB 트랜잭션의 격리 수준을 Read Committed 정도로 가정하며 더 높은 격리 수준이 필요한 경우 낙관적, 혹은 비관적 락 중 하나를 사용하면 된다.

낙관적 락 Optimistic Lock

  • 대부분의 트랜잭션에서 충돌이 발생하지 않는다고 낙관적으로 가정
  • DB의 Lock이 아닌 JPA의 버전 관리 기능을 사용, 애플리케이션이 제공하는 락
  • 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다.

비관적 락 Pessimistic Lock

  • 트랜잭션의 충돌이 발생한다고 가정하고 일단 락을 건다.
  • DB가 제공하는 락을 사용
    • 대표적으로 select for update 구문
  • DB 트랜잭션 범위를 넘어서는 두 번의 갱신 분실 문제 second lost updates problem가 있다.

    트랜잭션 A, B가 동시에 같은 데이터를 수정하고 A가 먼저 수정 완료한 후 B가 수정을 완료할 때
    먼저 완료한 A의 수정 사항은 사라지고 나중에 완료한 B의 수정 사항만 남는 것

  • 이 문제는 DB 트랜잭션의 범위를 넘어서기 때문에 아래 방법 중 하나를 선택해 해결한다.
    • 마지막 커밋만 인정 : A의 수정은 무시, B가 수정한 것만 인정
    • 최초 커밋만 인정 : A가 이미 수정을 완료했으므로 B가 수정 완료할 때 오류 발생
    • 충돌하는 갱신 내용 병합 : A, B의 수정 사항을 병합
    • 마지막 커밋만 인정하는 것이 기본이지만 때에 따라 최초 커밋만 인정하기가 더 합리적일 수 있다.
      • JPA의 버전 관리 기능을 사용하면 최초 커밋만 인정하기 구현 가능

@Version

  • JPA의 낙관적 락을 사용하려면 @Version 어노테이션으로 버전 관리 기능을 추가한다.
  • @Version 적용 가능 타입

    Long(long)
    Integer(int)
    Short(short)
    Timestamp

  • 버전 관리 기능 적용 예
    @Entity
    public class Board {
      @Id
      private Long id;
      private String title;
      
      @Version
      private Integer version;
      ...
    }
    • Entity에 버전 관리 필드(여기서는 version)를 하나 추가하고 @Version을 붙이면 된다.
      • 이후 Entity를 수정할 때마다 버전이 하나씩 자동으로 증가하고 수정할 때 조회 시점의 버전과 수정 시점의 버전이 다르면 예외가 발생
      • 트랜잭션 1, 2 모두 조회할 때는 제목 A인 데이터의 버전이 1
      • 트랜잭션 2가 먼저 수정을 완료하고 commit하면 버전이 2로 변경
      • 이후에 트랜잭션 1이 수정하고 commit할 때 원래 버전과 다르므로(1에서 2로 변경) 예외가 발생
      • 이렇게 최초 커밋만 인정하기를 적용할 수 있음

버전 비교 방법

  • Entity를 수정하고 트랜잭션을 커밋하면 영속성 컨텍스트를 플러시하면서 update 쿼리를 아래와 같이 실행하고 버전을 사용하는 Entity면 검색 조건에 버전 정보를 추가
    UPDATE board
    SET title = ?,
        version = ? #version + 1
    WHERE
        id = ?
        AND version = ? #version 비교
  • Entity의 값을 변경하면 버전이 증가한다.
    • embedded 타입과 값 타입 컬렉션은 논리적인 개념상 해당 엔티티의 값이므로 이것들도 수정하면 Entity의 버전이 증가한다.
    • 연관관계 필드를 외래 키를 관리하는 연관관계의 주인 필드를 수정할 때만 버전이 증가
  • 버전 관리 필드는 JPA가 직접 관리하므로 임의로 수정하면 안된다.
    • 강제로 증가하려면 특별한 락 옵션을 사용한다.

JPA 락 사용

JPA 사용시 추천 전략은 Read Committed + 낙관적 버전 관리

  • Lock을 적용하는 곳
    EntityManager.lock(), EntityManager.find(), EntityManager.refresh()
    Query.setLockMode() //TypeQuery 포함
    @NamedQuery
  • Lock을 즉시 적용
    Board board = em.find(Board.class, id, LockModeType.OPTIMISTIC);
  • 필요할 때 적용
    Board board = em.find(Board.class, id);
    ...
    em.lock(board, LockModeType.OPTIMISTIC);
    ...
  • javax.persistence.LockModeType에 정의된 Lock 옵션
    • 낙관적
      • OPTIMISTIC : 낙관적 락 사용
      • OPTIMISTIC_FORCE_INCREMENT : + 버전 정보 강제로 증가
    • 비관적
      • PESSIMISTIC_READ : 비관적 읽기 락 사용
      • PESSIMISTIC_WRITE : 비관적 쓰기 락 사용
      • PESSIMISTIC_FORCE_INCREMENT : 비관적 락 + 버전 정보 강제로 증가
    • 기타
      • NONE : 락 없음
      • READ : JPA 1.0 호환용, OPTIMISTIC과 동일하므로 OPTIMISTIC 사용
      • WRITE : JPA 1.0 호환용, OPTIMISTIC_FORCE_INCREMENT와 동일

JPA 낙관적 락

  • JPA가 제공하는 낙관적 락을 사용하려면 version이 필요함
  • 트랜잭션 커밋 시점에 충돌을 알 수 있다는 특징
  • 락 옵션 없이 @Version만으로도 낙관적 락이 적용되지만 락 옵션을 사용하면 더 세밀하게 제어 가능

NONE

  • 락 옵션 없이 @Version만으로 낙관적 락 적용
  • 조회 시점부터 수정 시점까지 데이터가 변경되지 않음을 보장
  • 엔티티 수정시 버전을 체크하면서 증가하고 이 때 DB의 버전과 현재 버전이 다르면 예외 발생
  • 두 번의 갱신 분실 문제 예방

OPTIMISTIC

  • NONE에서는 엔티티 수정시 버전을 체크하지만 OPTIMISTIC 옵션에서는 엔티티 조회시에도 버전 체크
  • 조회한 엔티티는 트랜잭션이 끝날 때까지 다른 트랜잭션에 의해 변경되지 않아야 한다.
    • 조회 시점부터 트랜잭션 종료까지 조회한 엔티티가 변경되지 않음을 보장
  • 트랜잭션 커밋시 버전 정보를 select 쿼리로 조회, 현재 엔티티의 버전과 같은지 검증하고 다르면 예외 발생
  • Dirty Read, Non-Repeatable Read 방지

OPTIMISTIC_FORCE_INCREMENT

  • 낙관적 락을 사용하면서 버전 정보를 강제로 증가
  • 논리적인 단위의 엔티티 묶음을 관리한다.
    • 예를 들어 팀과 멤버가 OneToMany, ManyToOne 양방향 연관관계를 가지고 멤버가 연관관계의 주인일 때
    • 팀 자체를 수정하는 것이 아닌 멤버만 추가하면 팀의 버전은 증가하지 않음
    • 팀은 물리적으로 변경되지 않았지만 논리적으로는 변경된 것이므로 이 때 팀의 버전을 강제로 증가할 때 사용
  • 엔티티를 직접 수정하지 않아도 커밋할 때 UPDATE 쿼리를 사용해 버전 정보를 강제로 증가
    • 이 때 DB의 버전이 엔티티의 버전과 다르면 예외 발생
    • 추가로 엔티티를 수정하면 버전 UPDATE가 발생하므로 총 두 번 증가할 수 있음

JPA 비관적 락

  • DB 트랜잭션 락 메커니즘에 의존하는 방법
  • 보통 SQL 쿼리에 select for update 구문을 사용해 시작하고 버전 정보는 사용하지 않음
  • 주로 PESSIMISTIC_WRITE 옵션 사용
  • 특징
    • 엔티티가 아닌 스칼라 타입을 조회할 때도 사용이 가능
    • 데이터를 수정하는 즉시 트랜잭션 충돌을 감지

PESSIMISTIC_WRITE

  • DB에 쓰기 락을 걸 때 사용하며 비관적 락이라고 하면 보통 이 옵션
  • DB에 select for update를 사용해 락을 건다.
  • 락이 걸린 로우는 다른 트랜잭션이 수정할 수 없으므로 Non-Repeatable Read를 방지

PESSIMISTIC_READ

  • 데이터를 반복 읽기만 하고 수정하지 않는 용도의 락으로 보통 사용하지 않음
  • 대부분의 DB는 dialect에 의해 PESSIMISTIC_WRITE으로 동작
    • MySQL : lock in share mode
    • PostgreSQL : for share

PESSIMISTIC_FORCE_INCREMENT

  • 비관적 락 중 유일하게 버전 정보 사용, 강제로 버전 정보를 증가
  • hibernate은 nowait을 지원하는 DB에 대해 for update nowait 옵션을 적용
    • Oracle : for update nowait
    • PostgreSQL : for update nowait
    • nowait을 지원하지 않으면 for update 사용

비관적 락과 타임아웃

  • 비관적 락을 사용하면 락을 획득할 때까지 트랜잭션이 대기하는데 무한정 기다릴 수 없기 때문에 타임아웃 시간을 줄 수 있음

  • 지정한 시간을 초과 후 응답이 없으면 javax.persistence.LockTimeoutException 예외 발생

    Map<String, Object> properties = new HashMap<>();
    //타임아웃 10초까지 대기 설정
    properties.put("javax.persistence.lock.timeout", 10000);
    
    Board board = em.find(Board.class, "boardId",
        LockModeType.PESSIMISTIC_WRITE, properties);
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @QueryHints({@QueryHint(name="javax.persistence.lock.timeout", value="10000")})
    Optional<Board> findById(Long boardId);
  • 타임아웃은 DB에 따라 동작하지 않을 수도 있음

0개의 댓글