JPA 에서는 기본적으로 영속성 컨텍스트(1차 캐시)를 사용하여 엔티티 사용시 application 레벨에서의 Repeatable Read 수준의 격리 수준을 지원합니다.
하지만, Repeatable Read 수준이 아닌 좀 더 엄격한 격리 수준이 필요할 때가 있습니다. 예를 들면, 쇼핑몰의 재고 관리 로직의 경우, 트랜잭션 시작 시점에 재고가 남아 있더라도, 트랜잭션 도중 다른 트랜잭션에 의해 재고가 사라질 수 있으므로 해당 트랜잭션을 그대로 커밋하는 경우 재고가 없지만 재고를 감소시키는 논리적 오류가 발생할 수 있습니다.
JPA 에서는 이런 경우를 방지하기 위해 높은 격리 수준을 위한 낙관적 락과 비관적 락을 지원합니다.
낙관적 락
낙관적 락은 기본적으로 트랜잭션간 충돌이 적은 빈도로 일어날 것을 가정하고 락킹 처리를 하는 방법입니다.
해당 방식에서는 데이터베이스에서 제공하는 락을 사용하지 않고, 버전 관리를 통해 충돌을 감지하게 됩니다.
낙관적 락의 기본적인 동작 방식
낙관적 락에서는 엔티티 내부에 버전 필드를 정의하고, 트랜잭션 시작 시점에 조회한 버전과 트랜잭션 커밋 시점에 조회한 버전을 비교하여 충돌을 감지합니다.
만약 충돌이 발생한 경우, 트랜잭션 커밋이 실패하고, 예외가 발생합니다.
출처 - 자바 ORM 표준 JPA 프로그래밍(김영한 저)
LockModeType
낙관적 락에서는 기본적으로 @Version 어노테이션만 적용해도 버전관리를 통해 트랜잭션 충돌을 감지하지만, 트랜잭션 커밋 시점에 엔티티를 업데이트 할 때만 버전을 체크하게 됩니다. 이 경우, 특정 엔티티를 조회하였을 때, 종료 시점의 엔티티의 상태와 조회 시점의 엔티티의 상태가 다를 수 있습니다. 이 때 사용하는 것이 LockModeType 옵션입니다.
NONE
@Version 만 적용하면 해당 옵션이 적용됩니다.
- 용도 특정 엔티티를 수정하려고 할 때, 해당 트랜잭션 수행 도중 다른 트랜잭션에서 해당 엔티티에 대한 변경을 제한할 때 사용합니다. 즉, 수정 목적에서 엔티티 조회 시점부터 엔티티 수정 시점까지의 DB상의 엔티티의 동일한 상태를 보장하고 싶을 때 사용합니다.
- 동작 트랜잭션 커밋 시도시, DB 상의 엔티티 version 상태와 해당 트랜잭션 내의 엔티티의 version 상태를 비교합니다. 즉, 조회 시점의 DB 상의 엔티티 version 과 수정 시점의 엔티티 version 을 비교하고, version 이 같은 경우만 정상적으로 커밋되며, version 값이 증가(변경)됩니다.
- 이점 두 번의 갱신 분실 문제를 예방합니다.
OPTIMISTIC
@Version + LockModeType 의 OPTIMISTIC 옵션을 사용하여 적용합니다. 위의 NONE 옵션과 동일한 점은 트랜잭션 커밋 시점에 엔티티의 상태를 기존 상태와 비교하는 것이지만, 다른 점은 해당 트랜잭션 내부에서 엔티티가 수정된 경우에만 버전을 체크하느냐, 항상 체크하느냐의 차이가 있습니다.
- 용도 특정 엔티티를 조회할 때, 해당 트랜잭션 수행 도중 다른 트랜잭션에서 해당 엔티티에 대한 변경을 제한할 때 사용합니다. 즉, 엔티티 조회 시점부터 트랜잭션 커밋 시점까지의 DB상의 엔티티의 동일한 상태를 보장하고 싶을 때 사용합니다.
- 동작 NONE 옵션과 같지만, 해당 엔티티가 트랜잭션 내부에서 변경되지 않은 경우도 버전 체크를 하게 됩니다.
- 이점 Dirty Read, Non-Repeatable Read 를 방지합니다.
OPTIMISTIC_FORCE_INCREMENT
OPTIMISTIC 옵션은 변경되지 않은 엔티티 커밋시 버전 정보를 증가시키지 않지만, 해당 옵션에서는 반드시 버전 정보를 증가시킵니다. 즉, 어떤 상황에서도 버전 체크와 버전 정보의 증가가 수행됩니다.
- 용도 논리적인 단위의 엔티티 묶음을 관리할 때 사용합니다. 예를 들어, 게시글과 댓글이 1 : N 의 관계에 있다고 가정해 봅시다. 이 상황에서 게시글에 달린 댓글이 추가되거나, 삭제되어도 다른 옵션에서는 아무런 버전의 충돌이 일어나지 않습니다. 하지만, 댓글의 CUD 연산시 해당 옵션을 적용한다면, 게시글의 버전이 증가하여 논리적인 엔티티의 동일한 상태를 보장할 수 있습니다.
- 동작 OPTIMISTIC 옵션과 같지만, 트랜잭션 커밋시 항상 버전이 증가합니다.
간단한 적용 - @Version 만 사용
Entity
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@Version
private Long version;
public static Post of(String title, String content){
Post post = new Post();
post.title = title;
post.content = content;
return post;
}
public void updateTitle(String title){
this.title = title;
}
public void updateContent(String content){
this.content = content;
}
}
Test
아래 테스트에서는 병렬 스트림을 사용하여 총 10000 건의 업데이트를 수행하도록 구성하였습니다.
@Test
void optimisticLock(){
Post post = Post.of("title", "content");
postRepository.save(post);
IntStream.range(1, 10000)
.parallel()
.forEach(i -> postService.updatePost(post.getId(), new PostUpdateRequestDto("title" + i, "content")));
}
결과 - 예외 발생
아래 예외 메시지를 보면 해당 데이터(row) 가 변경되었기 때문에 업데이트에 실패하였다고 나와 있습니다.
발생하는 예외의 종류는 다음과 같습니다. javax.persistence.OptimisticLockException - JPA 예외 org.hibernate.StaleObjectStateException - hibernate 예외 org.springframework.orm.ObjectOptimisticLockingFailureException - Spring 예외
org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.cacheexample.config.Post#1] ... Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.cacheexample.config.Post#1] ...
비관적 락
비관적 락은 기본적으로 트랜잭션간 충돌이 자주 일어날 것으로 가정하고 락킹 처리를 하는 방법입니다.
해당 방식에서는 데이터베이스의 락을 직접 사용하여 트랜잭션간의 충돌을 감지합니다. 이 경우, 낙관적 락과 다르게 엔티티의 특정 속성(스칼라) 만을 조회할 때도 사용할 수 있으며, 트랜잭션 커밋 시점이 아닌 다른 트랜잭션에서 수정 시도가 이루어 지면 충돌이 감지됩니다.
비관적 락의 기본적인 동작 방식
비관적 락은 데이터베이스의 락을 사용하기 때문에, 기본적으로 데이터베이스의 락 매커니즘에 따라 동작하며, LockModeType 에 따라 공유 락과 배타 락을 사용하게 됩니다.
LockModeType
PESSIMISTIC_WRITE
데이터베이스의 배타 락을 사용하는 방법으로, 특정 트랜잭션에서 배타 락을 걸게 된다면 다른 트랜잭션에서 읽기, 쓰기 모두 락이 해제될때까지 대기해야 합니다.
PESSIMISTIC_READ
데이터베이스의 공유 락을 사용하는 방법으로, 특정 트랜잭션에서 공유 락을 걸게 된다며 다른 트랜잭션에서 읽기는 가능하지만, 쓰기는 락이 해제될때까지 대기해야 합니다.
PESSIMISTIC_FORCE_INCREMENT
위의 두 옵션은, 낙관적 락에서 사용하는 버전 정보와 아무 연관이 없습니다. 즉, 비관적 락에서 엔티티가 수정되고, DB에 반영이 되었더라도, 낙관적 락을 사용중인 다른 트랜잭션에서는 이를 감지할 방법이 없습니다.
이러한 문제를 해결하기 위해서 비관적 락을 사용함과 동시에 버전 정보를 강제로 증가시키는 방법입니다.