본문 바로가기
학습/DB

Transaction 격리수준

by 코동이 2021. 9. 26.

springframework의 @Transactinoal은 isolation을 제공한다. 격리단계라고도 부른다. 동시에 발생하는 데이터 접근을 어떻게 처리할 것인지를 정한다. 

 

총 5가지 종류를 제공한다.

 

DEFAULT(TransactionDefinition.ISOLATION_DEFAULT)
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED)
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED)
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ)
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE)

 

아래로 갈수록 격리단계가 강력해지고 정합성을 보장해준다. 하지만, 동시처리하는 성능이 떨어지게 된다. 올라갈수록 성능이 좋지만, 일부 데이터 정합성을 보장하지 못하는 경우가 발생한다.

 

DEFAULT

@Transactional(isolation = Isolation.DEFAULT)

각 DB의 기본 격리단계를 따른다. (MYSQL = REPEATABLE_READ)

 

@Transactional
public void get(Long id) {
  System.out.println(bookRepository.findById(id)); //breakpoint1
  System.out.println(bookRepository.findAll());
  
  System.out.println(bookRepository.findBy(Id)); //breakpoint2
  System.out.println(bookRepository.findAll());
}

@Test
void isolationTest() {
  Book book = new Book();
  book.setName("JPA 강의");
 
  bookRepository.save(book);
 
  bookService.get(1L);
 
  System.out.println(bookRepository.findAll()); //breakpoint3
}

 

 

1. 테스트를 실행하고 breakpoint 1에서 멈춘다.

bookRepository.save(book)으로 book을 저장하고 category가 null이다.

 

2. MYSQL 터미널에서 다음을 실행한다.

 

start transaction;

update book set category='none';

 

따라서, mysql에 하나의 트랜잭션이 만들어지고 mysql에서 조회하면 book의 category가 null에서 none으로 바뀐다.

 

3. breakpoint2에서 멈춘다.

category가 여전히 null이다. mysql과 jpa의 트랜잭션이 각각 별도의 트랜잭션이기 때문에 반영되지 않는다. jpa 트랜잭션을 아직 종료되지 않았다.

 

4. MYSQL 터미널에서 다음을 실행한다.

 

commit

 

5. get() 마지막 줄의 bookRepository.findAll()을 확인한다.

여전히 category는 null이다. 아직 트랜잭션이 종료되지 않았기 때문에, 반영되지 않는다.

 

6. breakpoint3로 간다.

마지막에는 cateogry가 none으로 업데이트 된다. 왜냐하면 bookService.get(1L) 메서드가 끝남과 동시에 트랜잭션이 종료되었기 때문에 별도로 조회한다.

 

READ_UNCOMMITED

 

@Transactional(isolation = Isolation.READ_UNCOMITTED)

 

커밋되지 않은 데이터를 읽을 수 있다.

 

 

@Transactional(isolation = Isolation.READ_UNCOMITTED)
public void get(Long id) {
  System.out.println(bookRepository.findById(id)); //breakpoint1
  System.out.println(bookRepository.findAll());
  
  //커밋되지 않아도 category = none이 읽힌다!
  
  System.out.println(bookRepository.findBy(Id)); //breakpoint2
  System.out.println(bookRepository.findAll());
}

@Test
void isolationTest() {
  Book book = new Book();
  book.setName("JPA 강의");
 
  bookRepository.save(book);
 
  bookService.get(1L);
 
  System.out.println(bookRepository.findAll()); //breakpoint3
}

 

Dirty Read라고도 하며, 3번 breakpoint2에서 category가 none임을 확인할 수 있다. 아직 4번의 commit이 되지 않았어도 데이터를 읽을 수 있다. 만약, jpa에서 커밋되지 않는 데이터를 이미 읽고 저장했다면, mysql에서 rollbaclk을 한다고 해도 jpa에는 의도치 않게 커밋되지 않는 데이터로 업데이트 된다. Book Entity에 @DynamicUpdate를 하면, 필요한 값만 업데이트하므로 category는 none으로 바뀌지 않고 null로 롤백된다.

 

<한계점>

하지만, 보통 Dirty Read는 의도치않게 정합성을 해칠 수 있기 때문에 운영에서 잘 사용하지 않는다.

 

 

 

READ_COMMITTED

 

@Transaction(isolation = Isolation.READ_COMMITTED)

 

커밋된 데이터만 읽을 수 있다

 

READ_COMMITTED가 커밋된 데이터를 읽는다면, 같은 값을 조회해도 외부에서 커밋에 의해 값이 변경되면 변경된 값을 조회해야 한다. 하지만, 커밋이 되어도 반영되지 않는다. findById()로 값을 가져오면 영속성 컨텍스트의 1차 캐시에 등록이 되므로 처음 값을 그대로 유지한다.

 

따라서 커밋된 데이터를 조회하고 싶다면, entityManager.clear()를 해서 영속성 컨텍스트를 비워주어야 한다. 그래야 커밋이 제대로 반영된 내용이 다시 1차 캐시에 저장된다. 

 

@Transaction(isolation = Isolation.READ_COMMITTED)
public void get(Long id) {
  System.out.println(bookRepository.findById(id)); //breakpoint1
  System.out.println(bookRepository.findAll());
  
  //커밋이 된 category = none이 읽히려면 영속성 컨텍스트를 비워주어야 한다!
  entityManager.clear();
  
  System.out.println(bookRepository.findBy(Id)); //breakpoint2
  System.out.println(bookRepository.findAll());
  
  entityManager.clear();
}

@Test
void isolationTest() {
  Book book = new Book();
  book.setName("JPA 강의");
 
  bookRepository.save(book);
 
  bookService.get(1L);
 
  System.out.println(bookRepository.findAll()); //breakpoint3
}

 

이제 breakpoint1에서는 category는 null이 나오지만, MYSQL에서 커밋된 이후 breakpoint2에서는 category가 none으로 나온다. entityManager.clear()를 통해 처음에 breakpoint1에서 1차 캐시에 저장된 book이 없어졌기 때문이다.

 

<한계점>

조작없이 반복적으로 같은 조회를 했는데 값이 바껴 있는 경우는 UNREPEATABLE_READ라고 한다. 반복적 조회로 기존에 예상된 값과 다른 값이 나오는 경우, 의도치 않는 쿼리를 날릴 수도 있다. 그래서, 상황을 해결하기 위해서 REPEATABLE_READ를 사용한다.

 

 


REPEATABLE_READ

@Transaction(isolation = Isolation.REPEATABLE_READ)

트랜잭션 내에서 같은 값을 조회하면 항상 같은 결과가 나온다

 

아무리 중간에 다른 곳에서 커밋을 했어도, REPEATABLE_READ에서는 초기에 조회된 상태를 스냅샷으로 보관하고 있어서 트랜잭션이 끝나기 전까지, 계속 반복 조회시 같은 값이 나오도록 한다. entityMnager.clear()를 지속적으로 발생시켜도, 초기에 스냅샷을 계속 이용한다. 트랜잭션이 완전히 종료 된 뒤에야, category가 none으로 변경된다.

 

@Transaction(isolation = Isolation.REPEATABLE_READ)
public void get(Long id) {
  System.out.println(bookRepository.findById(id)); //breakpoint1
  System.out.println(bookRepository.findAll());
  
  //영속성 컨텍스트를 비워줘도 무조건 처음 조회된 캐시를 계속 조회한다!
  entityManager.clear();
  
  System.out.println(bookRepository.findBy(Id)); //breakpoint2
  System.out.println(bookRepository.findAll());
  
  entityManager.clear();
}

@Test
void isolationTest() {
  Book book = new Book();
  book.setName("JPA 강의");
 
  bookRepository.save(book);
 
  bookService.get(1L);
 
  System.out.println(bookRepository.findAll()); //breakpoint3
}

 

<한계점>

팬텀리드 현상 : 트랜잭션 내에서 보이지 않았던 객체가 추가, 수정되는 현상

 

public interface BookRepository extends JpaRepository<Book, Long> {
 @Modifying
 @Query(value = "update book set category='none'", nativeQuery=ture)
 void update();
}

 

@Transactional
public void get(Long id) {
  System.out.println(bookRepository.findById(id)); //breakpoint1
  System.out.println(bookRepository.findAll());
  
  entityManager.clear();
  
  System.out.println(bookRepository.findBy(Id)); //breakpoint2
  System.out.println(bookRepository.findAll());
  
  //MYSQL에서 COMMIT한 새로운 book객체의 category도 변경한다!
  bookRepository.update();
  
  entityManager.clear();
}

 

category='none'은 이제 update()라는 메서드에서 처리하도록 하고, MYSQL에서 새로운 book 객체를 insert를 하고 commit한다. JPA의 트랜잭션에서는 MYSQL에서 추가한 book 객체가 확인되지 않는다. 왜냐하면 JPA 트랜잭션에서는 insert 구문이 없기 때문이다. 하지만 bookRepository.update() 순간에 이미 book이 추가되었기 때문에 실제 트랜잭션이 모두 완료된 이후에, 보이지 않았던 새로운 book 객체의 category도 같이 none으로 바뀐다.

 

 

 

SERIALIZABLE

@Transaction(isolation = Isolation.SERIALIZABLE)

 

최고 수준의 격리단계로 커밋이 일어나지 않으면 lock이 걸린다.

 

@Transactional
public void get(Long id) {
  System.out.println(bookRepository.findById(id)); //breakpoint1
  System.out.println(bookRepository.findAll());
  
  entityManager.clear();
  
  System.out.println(bookRepository.findBy(Id)); //breakpoint2
  System.out.println(bookRepository.findAll());
  
  bookRepository.update();
  
  entityManager.clear();
  
  //다른 곳에서 트랜잭션을 실행하고 있으면 끝날때까지 트랜잭션을 종료하지 않는다!
}

 

SERIALIZABLE이 설정된 트랜잭션은 다른 곳에서 동시에 트랜잭션이 있다면, 다른 트랜잭션을 무한적으로 기다리고 있다가 끝낸다. 따라서 데이터의 정합성은 100% 맞는다.

 

<한계점>

하지만 그만큼, 기다리는 시간이 길어서 효율적이지 못할 수도 있다.

 

일반적으로 운영에서는 READ_COMMITTED 혹은 REPEATABLE_READ만 사용한다.

반응형

'학습 > DB' 카테고리의 다른 글

@Query 활용하기 2  (0) 2021.09.26
@Query 활용하기 1  (0) 2021.09.26
db Transaction(롤백처리)  (0) 2021.09.26
JPA 값 타입  (0) 2021.09.18
객체지향 쿼리 언어  (0) 2021.09.17