본문 바로가기
spring/jpa

자바 ORM 표준 JPA 프로그래밍 - 16장 트랜잭션과 락, 2차 캐시

by 쭈꾸마뇽 2021. 9. 5.

트랜잭션과 락

트랜잭션과 격리 수준

트랜잭션은 원자성, 일관성, 지속성을 보장한다. 문제는 격리성인데 트랜잭션간에 격리성을 완전히 보장하려면 트랜잭션을 거의 차례대로 실행해야 한다.  이렇게 하면 동시성 처리 성능이 매우 나빠진다.  이런 문제로 인해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다.

  1. READ UNCOMMITTED
  2. READ COMMITTED
  3. REPEATABLE READ
  4. SERIALIZABLE

순서대로 READ UNCOMMITED의 격리 수준이 가장 낮고 SERIALIZABLE의 격리 수준이 가장 높다.  격리 수준이 낮을수록 동시성은 증가하지만 격리 수준에 따른 다양한 문제가 발생한다.

격리 수준에 따른 문제점은 다음과 같다

  • DIRTY READ
  • NON-REPEATABLE READ
  • PHANTOM READ
  1. READ UNCOMMITTED : 커밋하지 않은 데이터를 읽을 수 있다.  예를 들어 트랜잭션1이 데이터를 수정하고 있는데 커밋하지 않아도 트랜잭션 2가 수정중인 데이터를 조회할 수 있다.  이것을 DIRTY READ라 한다.  트랜잭션2가 DIRTY READ한 데이터를 사용하는데 트랜잭션1을 롤백하면 데이터 정합성에 심각한 문제가 발생할 수 있다.
  2. READ COMMITTED : 커밋한 데이터만 읽을 수 있다.  DIRTY READ는 발생하지 않지만 NON-REPEATABLE READ가 발생할 수 있다.  예를 들어 트랜잭션1이 회원 A를 조회중인데 갑자기 트랜잭션 2가 회원 A를 수정하고 커밋하면 트랜잭션1이 다시 회원 A를 조회했을 때 수정된 데이터가 조회된다.  이처럼 반복해서 같은 데이터를 읽을 수 없는 상태를 NON-REPEATABLE READ라 한다.
  3. REPEATABLE READ : 한번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다.  하지만 PHANTOM READ는 발생 할 수 있다.  예를 들어 트랜잭션1이 10살 이하의 회원을 조회했는데 트랜잭션2가 5살 회원을 추가하고 커밋하면 다시 트랜잭션1이 10살 이하의 회원을 조회했을 때 회원 하나가 추가된 상태로 조회된다.  이처럼 반복 조회시 결과 집합이 달라지는 것을 PHANTOM READ라 한다.
  4. SERIALIZABLE : 가장 엄격한 트랜잭션 격리 수준이다.  하지만 동시성 처리 성능이 급격히 떨어질 수 있다.

애플리케이션 대부분은 동시성 처리가 중요하므로 데이터베이스들은 보통 READ COMMITTED 격리 수준을 기본으로 사용하되 일부 중요한 비즈니스 로직에 더 높은 격리 수준이 필요하면 데이터베이스 트랜잭션이 제공하는 잠금 기능을 사용하면 된다.

낙관적 락과 비관적 락 기초

  • 낙관적 락 : 이름 그대로 트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정.  이것은 데이터베이스가 제공하는 락 기능을 사용하는 것이 아니라 JPA가 제공하는 버전 관리 기능을 사용한다.  쉽게 이야기 해서 애플리케이션이 제공하는 락이다.  낙관적 락은 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다는 특징이 있다.
  • 비관적 락 : 이름 그대로 트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 걸고 보는 방법이다.  이것은 데이터베이스가 제공하는 락 기능을 사용한다.  대표적으로 SELECT FOR UPDATE 구문이 있다.

@Version

낙관적 락을 사용하려면 @Version 어노테이션을 사용해서 버전 관리 기능을 추가해야한다.

@Entity
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String content;

    @Version
    private Long version;
}

엔티티를 수정할 때마자 버전이 하나씩 자동으로 증가한다.  그리고 엔티티를 수정할 때 조회시점의 버전과 수정 시점의 버전이 다르면 예외가 발생한다.

제목이 A이고 버전이 1인 게시물이 있다.  트랜잭션1은 이것을 제목 B로 변경하려고 조회했다.  이때 트랜잭션2가 해당 데이터를 조회해서 제목을 C로 수정하고 커밋해서 버전 정보가 2로 증가했다.  이후 트랜잭션1이 데이터를 제목 B로 변경하고 트랜잭션을 커밋하는 순간 엔티티를 조회할 때 버전과 데이터베이스를 현재 버전 정보가 다르므로 예외가 발생한다.  따라서 버전 정보를 사용하면 최초 커밋만 인정하기가 적용된다.

데이터베이스 버전과 엔티티 버전이 같으면 데이터를 수정하면서 동시에 버전도 하나 증가시킨다.  만약 데이터 베이스에 버전이 이미 증가해서 수정중인 엔티티의 버전과 다르면 UPDATE 쿼리의 WHERE 문에서 VERSION 값이 다르므로 수정할 대상이 없다.  이때는 버전이 이미 증가한 것으로 판단해서 JPA가 예외를 발생시킨다.

JPA 락 사용

JPA를 사용할 때 추천하는 전략은 READ COMMITTED 트랜잭션 격리 수준 + 낙관적 버전 관리다

락은 다음 위치에 적용할 수 있다.

다음 처럼 조회하면서 즉시 락을 걸 수도 있고

다음처럼 필요할 때 락을 걸 수도 있다.

JPA 낙관적 락

JPA가 제공하는 낙관적 락은 버전을 사용한다.  따라서 낙관적 락을 사용하려면 버전이 있어야 한다.  낙관적 락은 트랜잭션을 커밋하는 시점에 충돌을 알 수 있다는 특징이 있다.

락 옵션을 적용하지 않아도 엔티티에 @Version이 적용된 필드만 있으면 낙관적 락이 적용된다.

NONE

  • 용도 : 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경되지 않아야 한다.  조회 시점부터 수정 시점까지를 보장한다.
  • 동작 : 엔티티를 수정할 때 버전을 체크하면서 버전을 증가한다.  이때 데이터베이스의 버전값이 현재 버전이 아니면 예외가 발생한다.
  • 이점 : 두 번의 갱신 분실 문제를 예방한다

OPTIMISTIC

@Version만 적용했을 때는 엔티티를 수정해야 버전을 체크하지만 이 옵션을 추가하면 엔티티를 조회만 해도 버전을 체크한다.  쉽게 이야기해서 한번 조회한 엔티티는 트랜잭션을 종료할 때가지 다른 트랜잭션에서 변경하지 않음을 보장한다.

  • 용도 : 조회한 엔티티는 트랜잭션이 끝날 때까지 다른 트랜잭션에 의해 변경되지 않아야 한다.  조회 시점부터 트랜잭션이 끝날 때까지 조회한 엔티티가 변경 되지 않음을 보장한다.
  • 동작 : 트랜잭션을 커밋할 때 버전 정보를 조회해서 현재 엔티티릐 버전과 같은지 검증한다.  만약 같지 않으면 예외가 발생한다.
  • 이점 : OPTIMISTIC 옵션은 DIRTY READ와 NON-REPEATABLE READ를 방지한다.

OPTIMISTIC_FORCE_INCREMENT

낙관적 락을 사용하면서 버전 정보를 강제로 증가한다.

  • 용도 : 논리적인 단위의 엔티티 묶음을 관리할 수 있다.  예를 들어 게시물과첨부파일이 일대다, 다대일의 양방향 연관관계이고 첨부파일이 연관관계의 주인이다.  게시물을 수정하는 데 단순히 첨부파일만 추가하면 게시물의 버전은 증가하지 않는다.  해당 게시물은 물리적으로는 변경되지 않지만 논리적으로는 변경되었다.  이때 게시물의 버전도 강제로 증가하려면 OPTIMISTIC_FORCE_INCREMENT를 사용하면 된다.
  • 동작 : 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해서 버전 정보를 강제로 증가시킨다.  이때 데이터베이스의 버전이 엔티티의 버전과 다르면 예외가 발생한다.  추가로 엔티티를 수정하면 수정 시 버전 UPDATE가 발생한다.  따라서 총 2번의 버전 증가가 나타날 수 있다.
  • 이점 : 강제로 버전을 증가해서 논리적인 단위의 엔티티 묶음을 버전 관리할 수 있다.

 

 

2차 캐시는 작성중..

댓글