일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- kotlin
- jpa
- QueryDSL
- 김영한
- Thymeleaf
- http
- 스프링 핵심 원리
- Servlet
- Android
- pointcut
- 스프링
- transaction
- SpringBoot
- java
- 백준
- JDBC
- Spring Boot
- db
- 인프런
- springdatajpa
- 스프링 핵심 기능
- 알고리즘
- AOP
- JPQL
- 그리디
- spring
- Greedy
- Proxy
- 자바
- Exception
- Today
- Total
개발자되기 프로젝트
Transaction Manager - 4 : Isolation 본문
2021.07.04 - [JPA/영속성] - Transaction Manager-3 : @Transactional 옵션
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때문이다.
코드가 처리되는 순서대로 보자.
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
'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 |