저번 강의에서 놓친 것도 있고, 딱 5번영상까지가 트랜젝션의 격리수준 내용이라 (피곤하기도 해서) 오늘은 끝까지는 안 가고 다음 강의만 봤습니다.
이전 강의에서 놓친 내용
격리 수준을 저번 강의를 정리할 때 한 번 쭉 정리해서 전부 이해하기는 했는데, 강의에서는 테스트를 위해 격리수준 설명 이외의 몇 가지를 더 소개해주셨더라구요.
- 최초로 확인해보는 격리 수준은 MySQL의 기본값인 REPEATABLE_READ 입니다. Lv2 격리수준으로, where 절의 조건에 맞는 게 추가되면 다음 where 쿼리에 포함될 수 있는 phantom read 가 일어날 수 있습니다.
- READ_UNCOMMITED 가 최소 격리 수준 (Lv 0) 으로 더티 리드 부분에서 정리했던 거네요.
- DB 차원에서 정합성을 지켜주기 위해 락(lock)이 걸린다는 점을 까먹고 기술을 안 했던. 그래서 격리 수준이 올라갈수록 성능 문제가 있겠죠.
- 엔티티 클래스에 @DynamicUpdate 를 붙여야 제대로 확인할 수 있습니다. 이 태그가 붙으면 JPA에서는 바뀐 필드만 업데이트하게 됩니다. 그 말은 그렇지 않다면 모든 필드를 저장한다는 말로, category='none' 이 롤백되었는지 보려고 했던 테스트에는 부적합하죠.
READ_COMMITED
격리 명칭 (Isolation.격리_명칭) |
격리 수준 | Dirty Read | Non-repeatable Read | Phantom Read |
READ_COMMITED | Lv 1 | 🛡️ | ❗ | ❗ |
먼저 (이전 강의에서) DIRTY 문제가 수정되었는지 확인합니다.
그 다음으로 문제점을 확인해봅니다.
READ_COMMITED, 즉 다른 트랜젝션에서 커밋한 정보까지는 읽는다는 거고, 이게 바로 Non-repeatable Read 죠. 두 번째로 읽었을 때, 다른 커밋에 의해 정보가 바뀌어서 다른 값이 나올 수 있는 거니까요.
다만 JPA 에서는 바로는 재현되지 않습니다. 엔티티 캐시 (1차 캐시) 때문인데요, 그래서 아래 코드에서 외부 트랜젝션에서 category='none' 을 지정해서 커밋했는데도 cateogory=null 로 이전과 동일하게 출력됩니다.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void get(Long id) {
System.out.println(">>> " + bookRepo.findById(id));
System.out.println(">>> " + bookRepo.findAll());
// category = null 이 찍힘
// 외부 트랜젝션에서 category='none' 으로 업데이트한 뒤 commit
System.out.println(">>> " + bookRepo.findById(id));
System.out.println(">>> " + bookRepo.findAll());
// category = null 이 찍힘
}
원인이 캐시임을 알고 있으니까 clear() 를 해주면 되죠. em.clear() 를 해주면 예상던 대로 동작합니다.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void get(Long id) {
System.out.println(">>> " + bookRepo.findById(id));
System.out.println(">>> " + bookRepo.findAll());
// category = null 이 찍힘
em.clear();
// 외부 트랜젝션에서 category='none' 으로 업데이트한 뒤 commit
System.out.println(">>> " + bookRepo.findById(id));
System.out.println(">>> " + bookRepo.findAll());
// category = 'none' 이 찍힘
em.clear();
}
REPEATABLE_READ
격리 명칭 (Isolation.격리_명칭) |
격리 수준 | Dirty Read | Non-repeatable Read | Phantom Read |
REPEATABLE_READ | Lv 2 | 🛡️ | 🛡️ | ❗ |
이 다음 단계는, 위와 같은 불상사가 발생하지 않는, 즉 반복해서 데이터를 읽어도 똑같은 값이 나오는 게 보장되는 단계입니다. 위 코드를 REPEATABLE_READ 로 바꿔서 다시 실행하면 category = null 이 나오게 됩니다. 트랜젝션 시작 시의 스냅샷을 유지하고 그걸 반환한다고 하네요.
이제 REPEATABLE_READ 의 문제점인 phantom read 를 봐야겠죠. 환상읽기(ㅋㅋ)는 where 조건이 바뀌는 상황에 다른 row가 포함되는 것으로, 이걸 확인하기 위해... JPA에서는 확인이 어렵다네요... "모든 게 엔티티 기반으로 돌아가서 팬텀리드를 재현하기 어렵다" 라고 하십니다. 방법으로 이전에도 잠깐 나왔던 네이티브 쿼리를 사용합니다.
public interface BookRepo extends JpaRepository<Book, Long> {
@Modifying
@Query(value = "update book set category='none'", nativeQuery = true)
void update();
}
위의 업데이트 쿼리에는 조건문이 없습니다. 즉, 테이블의 모든 엔티티를 수정하는 update 명령인데 보통 이렇게는 쓰진 않겠지만 지금은 엔티티가 하나밖에 없는 상황이라 환상읽기를 확인해보는 용도로는 괜찮겠죠.
이번 시나리오는 아래와 같습니다. 먼저 한 번 조회를 해와서 엔티티가 하나밖에 없음을 보여준 뒤, 외부 트랜젝션으로 테이블에 row 를 하나 더 추가해준 뒤 커밋합니다. 그리고 위 update() 함수를 실행해서 외부 트랜젝션으로 추가된 row에도 반영됨을 확인합니다. 트랜젝션을 시작할 때는 없었던 row 를 환상읽기해버린 거죠.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void get(Long id) {
// 먼저 한 번 조회를 해옵니다.
// 엔티티는 이 시점에서 하나만 있습니다.
System.out.println(">>> " + bookRepo.findById(id));
System.out.println(">>> " + bookRepo.findAll());
em.clear();
// 이제 외부 트랜젝션에서
// start transaction;
// insert into book ("id", "name") values (2, 'jpa 강의 2');
// commit;
// 를 실행합니다.
bookRepo.update();
em.clear();
// 이제 업데이트를 합니다.
// update book set category='none' where true; 였죠?
// 이런 이상한 쿼리를 실행하는 일은 없겠지만, 아무튼
// 앞에서 조회했을 때는 하나였으니 하나만 업데이트되어야 할 거 같은데 그렇지 않습니다.
// 외부 트랜젝션에서 추가한 2번 엔티티까지 category='none'; 으로 업데이트되어 있습니다.
System.out.println(">>> " + bookRepo.findById(id));
System.out.println(">>> " + bookRepo.findAll());
}
시작시
>>> [Book(생략, id=1, name=JPA 강의, category=null, authorId=null)]
외부 트랜젝션에서 row 삽입 후 커밋
update 후
>>> [Book(생략, id=1, name=JPA 강의, category=none, authorId=null),
Book(생략, id=2, name=jpa 강의 2, category=none, authorId=null)]
SERIALIZABLE
격리 명칭 (Isolation.격리_명칭) |
격리 수준 | Dirty Read | Non-repeatable Read | Phantom Read |
SERIALIZABLE | Lv 3 (최상) | 🛡️ | 🛡️ | 🛡️ |
이제 이걸 막는 것도 봐야겠죠. 똑같은 코드를 SERIALIZABLE 로 바꿔서 실행해보면... 예상하시는 대로 하나만 업데이트됩니다. 재밌는 점은 외부 SQL의 커밋을 중간에 미리 하지 않은 채로 update() 를 실행하려고 하면... 강의에서는 락이 걸립니다 (MYSQL). 이미 진행중인 트랜젝션이 끝나기를 기다리는 건데, 직접 해보니 postgres는 안 그러네요. 생각해보면 이런 상황에서는 락을 걸 필요가 없긴 하죠.
아무튼 phantom read가 막혔다는 겁니다.
실제 환경에서는 당연하겠지만 Lv 0 인 READ_UNCOMMITED도, Lv 3인(최상) SERIALIZABLE 도 성능상의 문제로 잘 쓰이지 않는다고 하네요. Lv 3로 설정했다가 트래픽이 몰리면 버거워할수도 있으니까요.
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
피곤하니까 학습 후 한마디는 노코멘트 (이것도 코멘트 아닌가)
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 8 영속성 전이 (Cascade) - 패스트캠퍼스 챌린지 36일차 (0) | 2022.02.28 |
---|---|
JPA Ch 7 트랜젝션 매니저의 propagation - 패스트캠퍼스 챌린지 35일차 (0) | 2022.02.27 |
JPA Ch 7 영속성 - 트랜젝션 매니저 (3~4) - 패스트캠퍼스 챌린지 33일차 (0) | 2022.02.25 |
JPA Ch 7 영속성 - 트랜젝션 - 패스트캠퍼스 챌린지 32일차 (0) | 2022.02.24 |
JPA Ch 7 영속성 - 생명주기 - 패스트캠퍼스 챌린지 31일차 (0) | 2022.02.23 |