Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

개발자되기 프로젝트

Transaction Manager - 4 : Isolation 본문

JPA/영속성

Transaction Manager - 4 : Isolation

Seung__ 2021. 7. 5. 22:07

2021.07.04 - [JPA/영속성] - Transaction Manager-3 : @Transactional 옵션

 

Transaction Manager-3 : @Transactional 옵션

Transaction Manager - 2 Transaction Manager - 1  1. Transaction이란? Data Base에서 다루는 개념. DB명령어들의 논리적인 묶음이다. Transaction의 성질 : ACID A : atomatic(원자성), 부분적의 성공을 허용..

bsh-developer.tistory.com

 

4. READ_COMMITTED


 

READ COMMIT으로 수정하여 똑같이 실행하자.(wating에서 rollback)

@Transactional(isolation = Isolation.READ_COMMITTED)
    public void get(Long id){

 

 

commit된 내용만 읽을 수 있기 때문에, category는 계속 null로 출력되었다.

= dirty read 현상이 사라졌다!

 

>>Optional[Book(super=BaseEntity(createdAt=2021-07-04T21:49:52.369452, updatedAt=2021-07-04T21:49:52.369452), 
id=1, name=JPA 강의, authorId=null, category=null)]

>>>Optional[Book(super=BaseEntity(createdAt=2021-07-04T21:49:52.369452, updatedAt=2021-07-04T21:49:52.369452), 
id=1, name=JPA 강의, authorId=null, category=null)]

>>>[Book(super=BaseEntity(createdAt=2021-07-04T21:49:52.369452, updatedAt=2021-07-04T21:49:52.369452), 
id=1, name=JPA 강의, authorId=null, category=null)]

>>>>[Book(super=BaseEntity(createdAt=2021-07-04T21:49:52.369452, updatedAt=2021-07-04T21:50:07.001703),
id=1, name=바뀌나?, authorId=null, category=null)]

 

 

 

4-1) READ_COMMITTED 의 문제

 

update로직 제외하고 조회로직만 남겨두자.

 

  @Transactional(isolation = Isolation.READ_COMMITTED)
    public void get(Long id){

        System.out.println(">>>" + bookRepository.findById(id));
        System.out.println(">>>" + bookRepository.findAll());

        System.out.println(">>>" + bookRepository.findById(id));
        System.out.println(">>>" + bookRepository.findAll());

//        Book book = bookRepository.findById(id).get();
//        book.setName("바뀌나?");
//        bookRepository.save(book);
    }

 

get method안의 두 번째 break point에서 DB에서 commit을 해주자.

(transaction이 종료되기 전에 DB에서 다른 Transaction을 실행)

예상을 해보자면, READ_COMMITTED는 commit된 data를 불러올 수 있기 때문에

category는 none으로 바뀔 것 같다.

 

다음 break point로 이동해보자 findById()를 실행 했는데, category가 null이다..?

>>>[Book(super=BaseEntity(createdAt=2021-07-05T20:23:07.324653, updatedAt=2021-07-05T20:23:07.324653),
id=1, name=JPA 강의, authorId=null, category=null)]

끝까지 진행시켜 보자.

 

다음 findAll에서는 none으로 나온당

>>>>[Book(super=BaseEntity(createdAt=2021-07-05T20:23:07.324653, updatedAt=2021-07-05T20:23:07.324653), 
id=1, name=JPA 강의, authorId=null, category=none)]

 

이 예상치 못한 문제는 entity cache때문이다.

 

Entity Cache

https://github.com/bsh6463/BookManager bsh6463/BookManager Contribute to bsh6463/BookManager development by creating an account on GitHub. github.com 1. Entity Manager Entity Manager란?  Entity의..

bsh-developer.tistory.com

 

코드가 처리되는 순서대로 보자.

 

Test코드가 시작되면서 id가 1인 book이 생성될 때 category는 dafault로 null로 저장된다.

 

이 때 cache가 존재하지 않기 때문에 해당 entity에 대하여 cache가 생성된다.

 

이후 DB에서 Category를 none으로 변경하고 commit을 한다.

 

Read_Committed 조건이기 때문에 항상 commit된 data를 읽을 것 같다.

 

하지만 "id"로 찾는 경우는 1차 cache 에서 data를 우선적으로 가져온다.

 

따라서 DB에서 commit한 none이 아니라 1차 cache에 저장된 null이 불러와지는 것.

 

findAll()은 1차 cache를 활용하지 않기 때문에 category를 none으로 불러온 것이다.

 

 

 

4-2) 해결 방법

 

BookService class에 EntityManager를 직접 주입하고, entitymanager를 정리해주자.

 

중간에 entityMaganer.clear()를 실행해주자.

@Service
@RequiredArgsConstructor
public class BookService {

    private final BookRepository bookRepository;

    private final AuthorRepository authorRepository;

    private final EntityManager entityManager;

    @Transactional
    public void putBookAndAuthor(){

        Book book = new Book();
        book.setName("JPA 시작작");
        bookRepository.save(book); //DB insert

        Author author = new Author();
        author.setName("hyun");
        authorRepository.save(author); // DB insert

        throw new RuntimeException("오류 발생해서 DB commit 발생하지 않음.");

    }

    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void get(Long id){

        System.out.println(">>>" + bookRepository.findById(id));
        System.out.println(">>>" + bookRepository.findAll());

        entityManager.clear();

        System.out.println(">>>" + bookRepository.findById(id));
        System.out.println(">>>" + bookRepository.findAll());

        entityManager.clear();


    }

}

전과 같은 순서로 코드를 실행해주자. 

 

findById() 결과

>>>Optional[Book(super=BaseEntity(createdAt=2021-07-05T20:44:02.202135, updatedAt=2021-07-05T20:44:02.202135),
id=1, name=JPA 강의, authorId=null, category=none)]

findAll() 결과

>>>[Book(super=BaseEntity(createdAt=2021-07-05T20:44:02.202135, updatedAt=2021-07-05T20:44:02.202135),
id=1, name=JPA 강의, authorId=null, category=none)]

둘 다 의도한 대로 결과가 나왔다.

 

이처럼 반복적으로 data 조회를 했는데 값이 변경되는 경우가 발생할 수 있다.

 

이런 경우를 UnRepeatable Read 상태라 하며, 조작을 하지 않았지만 transaction 내에서 조회값이 달라질 수 있는 현상

 

반복적으로 조회를 했을 때, 값이 변경될수 있는 상태 = unrepeatable read t상태

 

Unrepeatable Read 상태를 해결할 수 있는 조건이 Repeatable_Read이다.

 

 

 

5. Repeatable_Read


Isolation 을 REPEATABLE_READ로 변경하자.

 

  @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void get(Long id){

        System.out.println(">>>" + bookRepository.findById(id));
        System.out.println(">>>" + bookRepository.findAll());

        entityManager.clear();

        System.out.println(">>>" + bookRepository.findById(id));
        System.out.println(">>>" + bookRepository.findAll());

        entityManager.clear();

    }

 

REPEATABLE_READ는 한 Transaction 내 에서 반복적으로 조회를 하더라도

 

항상 동일한 값을 return되도록 보장해준다.

 

transaction 중에 다른 transaction 에서 다른 값을 commit하더라도 repeatable_read상태 transaction은

 

transaction이 시작할 때 조회했던 data(snap shot)을 별도로 저장하고 있다가, transaction이 끝나기 전 까지는

 

그 snap shot 정보를 return해준다. 즉 다른 transaction이 끼어들 틈이 없다.

 

이 전과 동일하게 재현을 해보자. 다음과 같이 결과가 나온다.

 

해당 transaction이 종료된 이후인 findAll()에서 category가 변경되었다.

 

 

* REPEATABLE_READ의 문제점  : PHANTOM READ

 

phantom read를 강제로 발생시키자.

 

<BookRepository> : custom query생성

public interface BookRepository extends JpaRepository<Book, Long> {//enum타입, id타입

    //PHANTOM READ강제로 발생,custom query
    @Modifying
    @Query(value = "update book set category='none'", nativeQuery = true)
    void update();


}

<BookService> : method 수정, update 실행

   @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void get(Long id){

        System.out.println(">>>" + bookRepository.findById(id));
        System.out.println(">>>" + bookRepository.findAll());

        entityManager.clear();

        System.out.println(">>>" + bookRepository.findById(id));
        System.out.println(">>>" + bookRepository.findAll());

        bookRepository.update();
        
        entityManager.clear();

//        Book book = bookRepository.findById(id).get();
//        book.setName("바뀌나?");
//        bookRepository.save(book);
    }
    
}

이번엔 DB에서 update 대신에 insert를 해준다.

insert into book (id, name) values (2, 'jpa 강의2');

전체 흐름 및 결과는 아래처럼 나온다.

 

 

id = 1인 book의 category가 none으로 바뀐 것은 이해가 된다.

 

그런데 중간에 insert한  id = 2인 book 도  none으로 변경이 되었다.

 

transaction내에서 조회를 통해 id=1인 book을 확인했고, 해당 record에 대해서 update할 것으로 예상했으나,

 

id=2인 book에 대해서도  udpate가 되었다.(id= 2인 book은 중간에 조회되지 않았다..)

 

이처럼 데이터가 안보이는데 처리가 된 현상을 phantom read라 부른다.

 

 

6. SERIALIZABLE


isolation을 SERIALIZABLE로 변경하자.

   @Transactional(isolation = Isolation.SERIALIZABLE)
    public void get(Long id){

        System.out.println(">>>" + bookRepository.findById(id));
        System.out.println(">>>" + bookRepository.findAll());

        entityManager.clear();

        System.out.println(">>>" + bookRepository.findById(id));
        System.out.println(">>>" + bookRepository.findAll());

        bookRepository.update();

        entityManager.clear();

//        Book book = bookRepository.findById(id).get();
//        book.setName("바뀌나?");
//        bookRepository.save(book);
    }

}

COMMIT이 일어나지 않은 Transaction이 존재하면 lock을 통해 waiting하게 된다.

 

commit이 실행되어야만 추가 logic이 진행된다.

 

 

DB에서 Transaction을 실행하고 book(id=2)를 insert하였다

 

다음 break point로 넘어가려 했으나 DB에서 commit을 하지 않아 다음 단계로 넘어가지 않는다!

코드를 실행하면 다음과 같은 결과를 볼 수 있다.

 

Transaction 중간에 book(id=2)를 insert하였고, commit을 하지 않으면 다음 단계로 넘어갈 수 없다.

 

commit이후에 조회 과정에서 book(id=1), book(id=2)모두 조회가 가능했고

 

update()이후에 두 entity모두 none으로 변경되었다. 

 

즉, Data를 변경하려면 다른 transaction이 commit이 무조건 되어야 한다.

 

덕분에 data정합성은 높아지나 waiting이 너무 길어짐...성능 상 문제 발생

 

 

7. 결론


L 격리단계 사용빈도 REMARK
- DEFAULT   DB의 default 격리단계를 적용함
MySQL은 기본 격리단계 = REAPETABLE_READ
0 READ_UNCOMMITTED 잘 안씀
(데이터
정합성 문제)
transaction에서 변경된 내용이  commit되기 전
다른 transaction에서 값을 읽을 수 있도록 허용
*문제점 : dirty read -->@DynamicUpdate로 해결
1 READ_COMMITTED 주로 씀 commit되지 않은 변경 내용을
다른 transaction에서 읽는 것을 금지함.
*문제점 : 1차캐시 사용하는 경우(findById()) 의도지 않은 동작
2 REPEATABLE_READ 주로 씀 동일 transaction(#1) 내에서 반복적으로 조회를 할 경우
다른 transactino(#2)에서 commit을 하더라도
#1에서 조회하는 값은 변하지 않음. 
*문제점 :  phantom read, 보이지 않는 data가 변경될 수 있음. 
3 SERIALIZABLE 잘 안씀
(성능상 문제..)
Transation(#1) 진행 중 다른 Transaction(#2)에서 data를 변경한다면
Transaction(#2)에서 commit해야 다음으로 진행됨.
*문제점: Data 정합성 높으나 waiting이 너무 길어짐

 

GitHub


 

 

bsh6463/BookManager

Contribute to bsh6463/BookManager development by creating an account on GitHub.

github.com

 

'JPA > 영속성' 카테고리의 다른 글

Cascade Remove, Orphan Removal, Soft Delete, @Where  (0) 2021.07.08
Transaction Manager - 5 : Propagation  (0) 2021.07.06
Transaction Manager-3 : Isolation  (0) 2021.07.04
Transaction Manager - 2  (0) 2021.07.04
Transaction Manager - 1  (0) 2021.07.04
Comments