TransactionManager
- Transaction은 계속 나왔다. @Transactional를 사용할때나, 영혹성 컨택스트에서 Transaction이 끝날때 merge가 된다고 할때도... 그때마다 지나간 이론적인 내용을 살펴보겠다.
Transaction ?
- DB에서 명령어들의 논리적 묶음
- 자바에서 메서드를 통해 물리적인 기능들을 묶는거 처럼 DB에서는 Transaction의 단위로 물리적인 기능을 묶는다.
- ex) 물건 구매 행위에서 결제와 주문이 한 트랜젝션으로 묶여야한다. 결제는 성공했는데 주문이 안들어가면 돈만 빠져나가는 경우가 생길수 있기 때문에, 주문이 실패하면 rollback이 되어야 한다.
- All OR Nothing, 모 아니면 도
- 특성
- A 원자성
- 부분적인 성공은 허용하지 않음
- 송금을 실패하면 출금도 실패해야함.
- C 일관성
- 내가 송금을 하려면 돈이 있어야한다.
- Data 간의 정확성을 맞춰야한다.
- I 독립성
- 트랜젝션내 기능은 다른 트랜젝션의 영향을 끼치면 안된다.
- D 지속성
- 데이터는 영구적으로 가지고 있어야한다.
- A 원자성
Transaction 실습해보자.
@Service
@RequiredArgsConstructor
public class BookService {
private final BookRepository bookRepository;
private final AuthorRepository authorRepository;
public void putBookAndAuthor() {
Book book = new Book();
book.setName("JPA 시작하기");
bookRepository.save(book);
Author author = new Author();
author.setName("martin");
authorRepository.save(author);
}
}
@SpringBootTest
public class BookServiceTest {
@Autowired
private BookService bookService;
@Autowired
private BookRepository bookRepository;
@Autowired
private AuthorRepository authorRepository;
@Test
void transactionTest() {
bookService.putBookAndAuthor();
System.out.println("books : " + bookRepository.findAll());
System.out.println("authors : " + authorRepository.findAll());
}
}
- debug mode로 확인하면 중간중간에 query를 확인할수 있다.
))
- 이제 Transactional 걸어주자
@Transactional public void putBookAndAuthor() {
- putBookAndAuthor() 메서드가 끝나기 전까지, 즉 트렌젝션이 끝나기 전까지 쿼리를 때려도 Book Table과 Author Table에는 아무 정보가 들어오지 않는걸 확인 할 수 있다.
- 참고로 Transactional 없으면 save쿼리에 있는 Transactional 때문에 각각의 데이터가 DB 전달된다.
Transaction 끝에 오류가 나면?
...
...
...
throw new RuntimeException("오류나서 DB commit 발생안함");
@Test
void transactionTest() {
// 이런 코드는 지양하지만 학습을위한 Test기 때문에 이렇게 한다.
try{
bookService.putBookAndAuthor();
}catch (RuntimeException e){
System.out.println(">>>> " + e.getMessage());
}
System.out.println("books : " + bookRepository.findAll());
System.out.println("authors : " + authorRepository.findAll());
}
- All or Nothing 때문에 오류가 발생하면 save는 되지만 commit이 아닌 rollback이 때문에 DB에 저장되지 않는다.
- A 원자성
😡Transaction 잘못된 사용.
- RuntimeException(uncheckedException)이 Transaction 내에서 발생하면 Rollback 되지만 Exception(checkedException)은 Transcation 내에 있어도 그냥 Commit 되버린다.!!
- checkedException 반드시 개발자가 책임지고 Exception을 처리해야한다.
만약 그냥 checkException이라도 Rollback시키려면 아래 어노테이션을 적용하면된다.
@Transactional(rollbackFor = Exception.class)
같은 클래스의 Method호출 할때 Transaction 사용.
public void put(){ this.putBookAndAuthor(); } // @Transactional(rollbackFor = Exception.class) @Transactional void putBookAndAuthor() { Book book = new Book(); book.setName("JPA 시작하기"); bookRepository.save(book); Author author = new Author(); author.setName("martin"); authorRepository.save(author); throw new RuntimeException("오류나서 DB commit 발생안함"); // unchecked Exception }
위와같이 put Methed가 putBookAndAuthor()를 호출하면 Transactional이 무시된다.
isolation() : 격리()
- org.springframework.transaction.annotation.Transactional 에 있다.
- DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE 모두 5개의 격리 수준을 지원하고 있다.
DEFAULT : 특별히 설정하지 않으면 Mysql의 경우에는 REPEATABLE_READ 수준으로 설정된다.
Test 실행하고 JPA에서 Book을 Insert한 상태
Hibernate:
insert
into
book
(created_at, updated_at, author_id, category, name, publisher_id)
values
(?, ?, ?, ?, ?, ?)
-- mYsql에서 다른 트랜젝션을 실행 시켜서 독립성을 위반시켜보자.
start transaction ;
update book set category="none";
commit;
// Debug를 한번더 실행 시켜보면 JPA에서 실행한 트랜젝션에 영향을 끼치이 않을걸 볼 수 있다.
findAll>> [Book(super=BaseEntity(createdAt=2022-02-13T17:24:28.699, updatedAt=2022-02-13T17:24:28.699), id=1, name=JPA 강의, category=null, authorId=null)]
### READ_UNCOMMITTED
- commit되지 않은 Data들을 읽을 수 있다.
- durtyRead 라고 한다.
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void get(Long id){
System.out.println(">>findById>> " + bookRepository.findById(id));
System.out.println(">>findAll>> " + bookRepository.findAll());
System.out.println(">>findById>> " + bookRepository.findById(id));
System.out.println(">>findAll>> " + bookRepository.findAll());
Book book = bookRepository.findById(id).get();
book.setName("바뀔까?");
bookRepository.save(book);
}
start transaction ;
update book set category="none" where id=1;
rollback ;
- 중요한건 독립성을 침범한 DB transction이 commit을 하던 rollback을 하던 결국 name=바뀔까? category="none"으로 update 된다.
// 이유는 JPA 쿼리때문이다.
Hibernate:
update
book
set
created_at=?,
updated_at=?,
author_id=?,
category=?, // 여기는 이미 category = "none" 으로 변경되어 있다.
name=?,
publisher_id=?
where
id=?
- 이러한 문제가 생기면 아래와 같이 해결하면 된다.
@DynamicUpdate
public class Book extends BaseEntity {
update
book
set
updated_at=?,
name=?
where
id=?
- 이렇게 필요한 쿼리(name)만 update하게 된다.
- 여기 중요한거는 독립성을 해쳤다는 거고 정확성이 낮아졌다는 거다. 일반적으로 사용하지 않는다.
### READ_COMMITTED
@Transactional(isolation = Isolation.READ_COMMITTED)
public void get(Long id){
System.out.println(">>findById>> " + bookRepository.findById(id));
System.out.println(">>findAll>> " + bookRepository.findAll());
System.out.println(">>findById>> " + bookRepository.findById(id));
System.out.println(">>findAll>> " + bookRepository.findAll());
Book book = bookRepository.findById(id).get();
book.setName("바뀔까?");
bookRepository.save(book);
}
start transaction ;
update book set category="none" where id=1;
commit ;
- DB transction이 commit을 하면 name=바뀔까? category="none"으로 update 된다.
- rollback 하면 Db에서 실행된 쿼리는 rollback된다.
- 하지만 READ_COMMITTED 하더라도 영속성 컨텍스트의 cache때문에 원하는 순간에 원하는 값을 볼수 없다는 점도 있다.
- 조작을 하지 않았는데 조회되는 값이 변경되는 현상(unrepeatable read)이라고 한다.
### REPEATABLE_READ
- unrepeatable read 현상을 대비해서 나온 수준이 READ_COMMITTED의 이다.
- 연속적으로 Transaction을 돌리더라고 항상 같은 값을 출력한다.
- tanscation이 실행 할 떄 스냅샷을 계속 리턴해준다.
- 디폴트 수준이기는한대 그래도 문제는 있다.
- phantom read 구현하기위해 아직 안 배운 커스텀쿼리를 쓰겠다.
public interface BookRepository extends JpaRepository<Book, Long> {
@Modifying
@Query(value = "update book set category='none'", nativeQuery = true)
void update();
}
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void get(Long id) {
System.out.println(">>findById>> " + bookRepository.findById(id));
System.out.println(">>findAll>> " + bookRepository.findAll());
System.out.println(">>findById>> " + bookRepository.findById(id));
System.out.println(">>findAll>> " + bookRepository.findAll());
bookRepository.update();
}
- TEST
- 디버그를 실행 시키고 DB transaction 실행해서 id 2를 만들어주자.
start transaction ;
insert into book ('id','name') values (2,'jpa 강의 2');
- 다음 브레이킹포인트로 가자.
findAll>> [Book(super=BaseEntity(createdAt=2022-02-13T18:11:28.489, updatedAt=2022-02-13T18:11:28.489), id=1, name=JPA 강의, category=null, authorId=null)]
Hibernate:
update
book
set
category='none'
- insert했지만 아직은 Data가 ID : 1 밖에 없다. commit 해보자.
[Book(super=BaseEntity(createdAt=2022-02-13T18:20:07.023, updatedAt=2022-02-13T18:20:07.023), id=1, name=JPA 강의, category=none, authorId=null),
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=2, name=jpa 강의 2, category=none, authorId=null)]
- 최종 결과가 Book이 2개 잘 왔는데 두 데이터 모두 category=none으로 되어있다. 분명히 id 1이 있으때 update 했는데 id 2도 none으로 되었다.
- 경우에 따라 데이터가 안보이는데 처리되는것이 phantom read 라고 한다. 그래서 나온 최고수준의 격리수준이 SERIALIZABLE 이다.
### SERIALIZABLE
- @Transactional(isolation = Isolation.SERIALIZABLE) 추가하고 똑같이 TEST 해보자.
- 이렇게 하면 commit이 일어나지 않은 트랜젝션이 있으면 lock을 통해 waiting상태가 된다.
- 그럼 위 테스트를 실행 시켰을때 update 쿼리를 실행시키 전에 waiting하여 commit을 기다리고 commit이되면 넘어가게 된다.
- 정확성은 100%이지만 waiting이 길어져서 성능에 문제가 생길수 있다.
#### 😎😎트랜잭션은 정확성은 중요하다. 원하는 값이 나왔다고 하더라도 언제 쿼리가 어떻게 실행됬는지 알고있어야하며, durty read, unrepeatable read, phantom read 등의 문제점을 고려해야한다는 거다.
'Dev > JPA' 카테고리의 다른 글
JPA @Query (0) | 2022.03.02 |
---|---|
JPA OrphanRemoval(+@Where) (0) | 2022.03.02 |
JPA Cascade (0) | 2022.03.02 |
JPA에서 Transaction의 전파 (0) | 2022.03.02 |
영속성 컨텍스트(Entity LifeCycle) (0) | 2022.03.02 |