JPA - 트랜잭션과 락
트랜잭션은 ACID라 하는 원자성, 일관성, 고립성, 지속성을 보장해야 한다.
원자성(Atomicity): 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하든가 모두 실패해야 한다.
일관성(Consistency): 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.
고립성(격리성)(Isolation): 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않아야 한다.
지속성(Durability): 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템 에러가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구할 수 있어야 한다.
4가지 특징 중 고립성을 보장하기 위해 ANSI 표준은 트랜잭션 격리 수준을 4단계로 나눴다.
아래로 갈수록 격리 수준이 높아지고 동시성은 낮아진다.
1. READ UNCOMMITED(커밋되지 않은 읽기)
커밋되지 않은 데이터를 읽을 수 있다. 하지만 DIRTY READ가 발생할 위험이 있다. DIRTY READ란 어떤 트랜잭션 A에서 수정 중인 데이터를 다른 트랜잭션 B에서 조회할 수 있는 것이다. 만약 이렇게 되면 A가 수정을 완료한 후 커밋하면 갱신된 데이터와 B가 조회한 데이터하고 다른 일이 생겨 문제가 생길 수 있다.
2. READ COMMITTED(커밋한 데이터만 읽기)
커밋한 데이터만 읽을 수 있다. 즉, DIRTY READ 가 발생하지 않는다. 하지만 NON-REPEATABLE READ가 발생할 수 있다. 예를 들어 트랜잭션 A가 데이터를 조회 중인데 트랜잭션 B가 데이터를 수정하고 커밋해버리면 A가 다시 데이터를 조회했을 때에는 같은 데이터를 반복해서 읽을 수 없다는 것이다.
3. REPEATABLE READ(반복 가능한 읽기)
한번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다. 이는 어떤 트랜잭션에서 데이터를 조회 중일 때 그 데이터를 다른 트랜잭션에서 수정할 수 없다는 것을 의미한다. 하지만 PHANTOM READ는 발생할 수 있다. 이는 반복 조회 시 결과 집합이 달라지는 것을 의미한다. 이는 단순히 트랜잭션에서 조회 중일 때 데이터를 변경하는 것이 아닌 추가나 삭제를 진행하면 다시 조회했을 때 추가되거나 삭제된 데이터가 반영되어 결과 집합이 달라지게 된다는 것이다.
4. SERIALIZABLE(직렬화 기능)
여기서는 PHANTOM READ가 발생하지 않는다. 하나의 트랜잭션 중에서는 어떠한 트랜잭션이 일어날 수 없다는 것을 의미한다.
애플리케이션 대부분은 동시성 처리가 중요하므로 데이터베이스들은 보통 READ COMMITTED 격리 수준을 기본으로 사용한다. 만약 더 높은 격리 수준이 필요하면 데이터베이스 트랜잭션이 제공하는 LOCK 기능을 사용하면 된다.
JPA는 데이터베이스 트랜잭션 격리 수준을 READ COMMITTED 정도로 가정한다. 만약 일부 로직에 더 높은 격리 수준이 필요하면 낙관적 락과 비관적 락 중 하나를 사용하면 된다.
낙관적 락은 트랜잭션 대부분은 충돌이 발생하지 않는다고 가정하고 충돌이 났을 시에 예외처리를 하는 것이다.
두 유저가 특정 데이터를 수정할 때 커밋을 시간차를 두고 하였을 시에 마지막 커밋만을 인정할 것인지, 최초 커밋만 인정할 것인지, 충돌하는 갱신 내용을 병합할 것인지로 생각을 할 수 있다. 기본은 마지막 커밋만 인정이 된다. 하지만 최초 커밋만 인정하기를 사용하고 싶을 때가 있다. 이럴 때 사용하는 것이 @Version이다.
@Entity
public class Board {
@Id
private String id;
private String title;
@Version
priavte Integer version;
}
위의 예시처럼 엔티티에 version을 설정하면 데이터를 수정하고 커밋이 될 때마다 version이 1씩 증가하게 된다. 그리고 version이 커밋 시점에서 조회시점의 version과 다를 경우 예외가 발생한다. 그래서 최초 커밋이 인정되는 경우가 되는 것이다. version은 엔티티의 값을 변경하면 증가한다. 그리고 값 타입인 임베디드 타입과 값 타입 컬렉션은 논리적인 개념상 해당 엔티티의 값이므로 수정하면 엔티티의 버전이 증가한다. 단 연관관계 필드는 외래 키를 관리하는 연관관계의 주인 필드를 수정할 때만 버전이 증가한다.
예외는 아래와 같다.
javax.persistence.OptimisticLockException(JPA 예외)
org.hibernate.StaleObjectStateException(하이버네이트 예외)
org.springframework.orm.ObjectOptimisticLockingFailureException(JPA 예외)
@Version 만 사용해도 낙관적 락이 적용된다. 여기다 JPA 락을 이용하면 락을 더 세밀하게 제어할 수 있다.
@Lock를 repisitory 메서드나 seivice 메서드 등에 달아주면 된다.
@Transactional
@Lock(LockModeType.PESSIMISTIC_WRITE)
public int test(){
...
}
낙관적 락의 LockModeType를 살펴보면 아래와 같다.
Lock Modes
- OPTMISTIC
@Version만 적용했을 때는 엔티티를 수정해야 버전을 체크하지만 이 옵션을 추가하면 엔티티를 조회만 해도 버전을 체크한다. DIRTY READ와 NON-REPEATABLE READ를 방지한다. - OPTIMISTIC_FORCE_IMCREMENT
낙관적 락을 사용하면서 버전 정보를 강제로 증가한다. 일반적으로 연관관계에 있어서 연관관계의 주인이 아닌 필드를 변경할 때나 연관관계의 주인이 되는 필드를 수정하는 것이 아닌 추가할 경우에는 버전이 증가하지 않는다. 그럴 때 이 옵션을 적용하면 버전 정보를 강제로 증가시킬 수 있다. 만약 이 옵션을 사용하고 버전이 변경되는데 해당하는 필드 또한 수정된다면 버전은 2번 증가가 일어난다.
비관적 락은 트랜잭션 충돌이 발생한다고 가정하고 우선 락을 걸고 보는 것이다.
비관적 락의 LockModeType와 예외를 살펴보면 아래와 같다.
Lock Modes
- PESSIMISTIC_READ
데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용한다. - PESSIMISTIC_WRITE
일반적으로 가장 자주 사용하는 옵션이고 쓰기 락을 건다. NON-REPEATABLE READ를 방지하며 락이 걸린 로우는 다른 트랜잭션이 수정할 수 없다. - PESSIMISTIC_FORCE_INCREMENT
PESSIMISTIC_WRITE와 유사하게 작동하며 유일하게 엔티티의 버전 정보를 사용한다. 버전 정보를 강제로 증가시킨다.
Exception
- PersistenceException
한 번에 하나의 Lock만 얻을 수 있으며, 락을 가져오는데 실패하면 발생하는 예외이다. - LockTimeoutException
락을 기다리다 설정해놓은 wait time을 지났을 경우 발생하는 예외이다. - PersistanceException
영속성 문제가 발생했을 때의 예외이다.
참고
자바 ORM 표준 JPA 프로그래밍 - 김영한
https://doooyeon.github.io/2018/09/29/transaction-isolation-level.html
https://isntyet.github.io/jpa/JPA-%EB%B9%84%EA%B4%80%EC%A0%81-%EC%9E%A0%EA%B8%88(Pessimistic-Lock)/
https://velog.io/@kdhyo/JavaTransactional-Annotation-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-26her30h