DB 명령어들의 논리적인 묶음
-> 자바에서 메서드를 통해 논리적인 작업 단위 묶듯이!
원자성 (Atomicity)
-> All or Nothing
작업들이 중간에 중단되어도 일관성 보장을 의미 (모두 성공 or 실패)
ex) 돈을 송금하면 받는 계좌에 추가되어야 함
일관성 (Consistency)
데이터간에 정합성을 맞추는 의미 (데이터의 일관성 있는 상태로 유지)
ex) 송금을 하기 전 잔액이 0보다 많아야 하고 -> 송금 하면 계좌는 0이거나 0보다 많아야 함
독립성 (Isolation)
트랜잭션 수행 시 다른 트랜잭션의 연산을 못하도록 보장
고립성
이라고도 말하며, 성능 관련 이유로 가장 유연성 있는 제약조건
ex) 친구에게 송금을 하는 도중에(송금 트랜잭션 진행중, 완료X) 그 돈을 친구가 통장에서 돈 인출(인출 트랙잭션)이 먼저 실행되는 경우 -> 문제 발생
귀속성 (Durability)
성공적으로 수행된 트랜잭션은 영원히 반영 (데이터 영구 보관)
@Transactional
두 군데 에서 제공
-> javax.transaction
: 스프링에 의존없이 사용 가능 (다른 컨테이너 사용 가능)
org.springframework.transactional.annotaion
: 스프링에 많은 기능 사용 가능
@Transactional
public void BookAndAuthor(){
Book book = new Book();
book.setName("JPA 시작하기");
bookRepository.save(book);
Author author = new Author();
author.setName("martin");
authorRepository.save(author);
}
위 코드에 Transactional 어노테이션을 붙이지 않고 save(book)뒤에 브레이크 포인트를 두었을 때 db에 정상적으로 값이 반영됨
-> 붙이지 않을 시엔 select * from book
; 해도 empty
-> 블럭 내 코드들을 하나의 트랜잭션
으로 보았기에 save(author)가 끝난 후에야 DB에 반영 되는 것!
만약 BookAndAuthor() 코드 마지막에 throw new RunTimeException() 즉 Unchecked Exception
을 발생시켰다면
-> @Transactional가 붙은 경우에는 커밋이 안되는 반면
-> 붙이지 않았을 시에는 이미 save 과정에서 커밋이 되어 오류는 발생하지만 Db 에 반영될것이라 유추 가능
Checked Exception에 사용 (Checked - UnChecked에 혼용이 원인)
Checked Exception
명시적인 Exception 처리가 필요
Rollback 처리X
UnChecked Exception
예외가 발생하면 Rollback 처리O
즉 Check Exception을 처리하는 try-catch에서 DB에 반영되지 않게 롤백을 명시적으로 지정해줘야 원하는 결과 실행될 것
//TransactionAspectSupport.java
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
...
try {
retVal = invocation.proceedWithInvocation(); //트랜잭션이 선언된 메소드 실행
}
catch (Throwable ex) {
completeTransactionAfterThrowing(txInfo, ex); //Exception 발생시 Rollback 처리
throw ex;
}
unchecked 경우 getTransactionManager를 통해 rollback 치는 것을 볼 수 있다
//TransactionAspectSupport.java
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
...
/DefaultTransactionAttribute.java
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error); //RuntimeException or Error만 롤백
}
checked 의 경우 commit을 친다
//TransactionAspectSupport.java
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
...
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
...
else {
// We don't roll back on this exception.
// Will still roll back if TransactionStatus.isRollbackOnly() is true.
try {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
Checked Exception 을 Rollback 하는 방법 (rollbackFor 속성 사용)
...
public class BookService {
...
@Transactional(rollbackFor = Exception.class)
public void pubBookAndAuthor() throws Exception {
...
}
}
메소드에 참조하는 메소드가 @Transaction인 경우 (해당 메소드는 @Transaction X)
스프링 컨테이너는 빈으로 진입할 때 메소드에 걸려있는 어노테이션에 대해서만 처리
-> put() 에는 @Transactional 이 존재하지 않으므로 처리X
빈 클래스 내부에서 내부를 호출할때는 @Transactional 효과가 없음
...
public class BookService {
...
public void put(){
this.pubBookAndAuthor();
}
@Transactional
public void pubBookAndAuthor(){
...
throw new RuntimeException("오류 발생 commit 실패");
}
}
//테스트 실행 결과 (Rollback 실패되고 DB에 값이 반영됨)
동시에 발생하는 트랜잭션 간에 데이터 접근을 어떠한 식으로 처리할것
인지 나타내는것이 격리 단계(두 트랜잭션 값 경합 시)
일반적으로 READ_COMMITTED, REPEATABLE_READ를 많이 사용(정합성과 성능상의 이유)
DEFAULT: 데이터베이스에 격리 단계를 사용(MySQL default: REPATABLE_READ
)
0단계 : Default
1단계 : READ_UNCOMMITTED
: 다른 트랜잭션 수행에 커밋되지 않은 결과를 조회(Dirty Read)
2단계 : READ_COMMITTED
: 다른 트랜잭션에서 커밋된 결과를 조회 (Unrepeatable 상태)
3단계 : REPEATABLE_READ
4단계 : SERIALIZABLE
단계가 높을수록 정합성을 보장해주는 반면 동시 처리 수행 성능이 떨어짐
낮아질수록 성능은 높지만 정합성을 보장해주지 못하는 경우가 간혹 발생
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void get(Long id){
// 1st breakpoint
System.out.println(" >>> " + bookRepository.findById(id));
System.out.println(" >>> " + bookRepository.findAll());
// 2rd breakpoint
System.out.println(" >>> " + bookRepository.findById(id));
System.out.println(" >>> " + bookRepository.findAll());
}
데이터 조회 테스트 코드
@Test
void isolationTest(){
Book book = new Book();
book.setName("JPA 기본 책");
bookRepository.save(book);
bookService.get(1L);
// 3rd breakpoint
System.out.println(">>> " + bookRepository.findAll());
}
}
1st breakpoint 실행 이후 mysql 터미널에서 transaction 실행
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update book set category = 'none';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
이후 2rd breakPoint 실행 시
id=1, name=JPA 기본 책, category=none, authorId=null
update가 반영 된것처럼 실행 됨(아직 commit 되지 않은 데이터가 조회되었다는 의미의 dirty read)
-> 데이터베이스를 이용한 쿼리가 커밋되지 않았는데 결과가 jpa 트랜잭션 반영
데이터 수정 테스트 코드
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void get(Long id){
System.out.println(">>> " + bookRepository.findById(id)); //1 브레이크 포인트
System.out.println(">>> " + bookRepository.findAll());
System.out.println(">>> " + bookRepository.findById(id)); //2 브레이크 포인트
System.out.println(">>> " + bookRepository.findAll());
Book book = bookRepository.findById(id).get();
book.setName("바뀔까");
bookRepository.save(book);
}
mysql 트랜잭션 생성
start transaction;
update book set category='none';
commit;
name만 바꿀려 했는데 category = none 까지 반영 되는걸 볼 수 있다
jpa update 특징이 반영되어있다
// 테스트 로그
update
book
set
updated_at=?,
author_id=?,
category=?,
name=?,
publisher_id=?
where
id=?
// 1 브레이크 포인트에서 데이터베이스 트랜잭션 시작 후 update 실행
// 두번째 브레이크 포인트에서 세번째로 넘어갈때 트랜잭션 락이 발생
데이터베이스에서 commit/rollback 실행하면 락이 해제
// commit과 rollback이 아래와 같은 동일한 결과를 반환
// 이유는 jpa에서 commit되지 않은 값을 가지고 있다가 save에서 모두 반영하기 때문
>>> [Book(super=BaseEntity(createdAt=2022-11-21T21:31:46.314764, updatedAt=2022-11-21T21:32:36.307645), id=1, name=바뀔까?, category=none, authorId=null)]
Book 위에다가 @DynamicUpdate
선언 후 mysql에서 rollback 실행 시category = 'none'이 반영안되는 것을 볼 수 있다
commit 시에는 category 'none' 반영
-> update 쿼리엔 실행안되지만 이후 Binding Parameter 에 나타남
Hibernate:
update
book
set
updated_at=?,
name=?
where
id=?
>>> [Book(super=BaseEntity(createdAt=2022-11-21T21:44:51.986046, updatedAt=2022-11-21T21:45:30.507843), id=1, name=바뀔까?, category=null, authorId=null)]
isolation.READ_UNCOMMITTED 를 해결하기 위해 등장, 테스트 방법은 READ_UNCOMMITTED
와 동일
BookService.java 에 @Transactional(isolation = Isolation.READ_COMMITTED) 로 수정
Book.java에 @DynamicUpdate를 제거 (커밋된 것만 조회하므로 필요 없음)
//DynamicUpdate를 제거했기 때문에 모든 컬럼에 대해 update
Hibernate:
update
book
set
updated_at=?,
author_id=?,
category=?,
name=?,
publisher_id=?
where
id=?
//Dirty Read 현상 제거 -> Rollback시 반영 안됨
>>> [Book(super=BaseEntity(createdAt=2022-11-21T21:54:32.964687, updatedAt=2022-11-21T21:54:50.025090), id=1, name=바뀔까?, category=null, authorId=null)]
문제점
@Transactional(isolation = Isolation.READ_COMMITTED)
public void get(Long id){
System.out.println(">>> " + bookRepository.findById(id)); // 1 브레이크
System.out.println(">>> " + bookRepository.findAll());
// entityManager.clear(); //JPA cache로 인해 예상결과와 달라 clear() 실행
System.out.println(">>> " + bookRepository.findById(id)); // 2 브레이크
System.out.println(">>> " + bookRepository.findAll());
// entityManager.clear(); //JPA cache로 인해 예상결과와 달라 clear() 실행
}
1브레이크에서 update
후 2브레이크에서 commit
하였음
entityManager.clear()를 하지 않고 그냥 실행 시 2브레이크에서 commit을 했음에도 불구하고 1,2브레이크에서 category = null로 조회됨
-> JPA Cache
로 인해 unrepeatble read(반복적으로 조회시 값이 변경 될 수 있는 상태) 상태가 됨
이를 해결 하기 위한 것이 repeatable read
반복해서 값을 조회하더라도 항상 동일한 값이 리턴 되도록
다른 트랜잭션에서 commit된 값이 발생하더라도 별도의 스냅샷
, 즉 커밋된 데이터를 직접 가져오는 것이 아니라 자기 트랜잭션이 시작할때 조회했던 데이터를 별도로 저장하고 있다가 이 트랜잭션이 끝나기 전까지 스냅샷을 계속해서 리턴
문제점
팬텀 리드 (Phantom read)
: 트랜잭션 내에서 같은 쿼리를 실행하지만 예상결과와 다른 결과가 나오는 현상이 발생
public interface BookRepository extends JpaRepository<Book, Long> {
@Modifying
@Query(value = "update book set category = 'none'", nativeQuery = true)
// jpa는 entity기반 이기에 무조건 insert 후 update
void update();
}
test 코드
System.out.println(" >>> " + bookRepository.findById(id)); // break1
System.out.println(" >>> " + bookRepository.findAll());
entityManager.clear();
System.out.println(" >>> " + bookRepository.findById(id)); // break2
System.out.println(" >>> " + bookRepository.findAll());
bookRepository.update();
entityManager.clear();
mysql 쿼리
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into book(`id`, `name`) values(2, 'jpa lecture2');
Query OK, 1 row affected (0.00 sec)
break 1에서 위의 insert문 실행하고 3포인트로 넘어갈 때 commit 실행
id=1, name=JPA 기본 책, category=none, authorId=null), Book(super=BaseEntity(createdAt=null, updatedAt=null), id=2, name=jpa 강의 2, category=none, authorId=null)]
하지만 id2의 카테고리 값까지 none이 추가됨(예상 : 한개의 쿼리만 none으로)
-> 이런것이 Phantom Read 상태라고 함
commit이 일어나지 않은 트랜잭션이 존재하면 Lock을 통해 웨이팅을 하게됨
-> commit이 실행 되어야만 로직 실행 됨
1포인트에서 2포인트로 넘어가는 시점부터 락이 바로 걸림
isolation.REPEATABLE_READ 를 해결하기 위해 등장, 테스트 소스 및 구성은 동일
다른 트랜잭션이 끝날때 까지 무조건 기다리고 처리가 완료되면 실행 (데이터 정합성 100%)
웨이팅이 길어져서 성능에는 안좋은 영향 발생
트랜잭션의 시작과 끝은 각 메서드에 시작과 끝과 같음
한 메서드(현재 트랜잭션) 내에서 다른 메서드(다른 트랜잭션)를 호출한다면
트랜잭션을 어떻게 처리하는지에 대한 교통정리를 하는 것이 Propagation()
Spring @Transactional
은 Propagation.java의 7가지 설정 지원 (default: REQIRED)
기존에 사용하는 트랜잭션이 있으면 그것을 재사용, 없으면 새로운 트랜잭션 생성
JPA Repository에 save()가 REQUIRED전파를 사용 (코드블럭 내에선 동일 트랜잭션)
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
앞서 설명했을 때 @Transactional이 붙은 메서드 안의 save는 메서드 내 하나의 트랜잭션에서 동작하는 반면
@Transactional이 붙지 않은 메서드 안에 save 메서드 한줄한줄이 각각 트랜잭션이 생성되고 commit 된다고 한 이유 -> REQUIRED
UnChecked Exception이 일어나면 전파된 트랜잭션 모두 rollback
테스트 코드
@Test
void transactionTest(){
try{
bookService.putBookAndAuthor();
} catch(RuntimeException e){
System.out.println(">>> " + e.getMessage());
}
System.out.println("books : " + bookRepository.findAll());
System.out.println("authors : " + authorRepository.findAll());
}
//BookService.class
@Transactional(propagation = Propagation.REQUIRED)
public void putBookAndAuthor() {
Book book = new Book();
book.setName("JPA 시작하기");
bookRepository.save(book);
try {
authorService.putAuthor();
} catch(RuntimeException e) {
System.out.println(e.getMessage());
}
//throw new RuntimeException("오류가 발생하였습니다");
}
//AuthorService.class
@Transactional(propagation = Propagation.REQUIRED)
public void putAuthor() {
Author author = new Author();
author.setName("martin");
authRepository.save(author);
//throw new RuntimeException("오류가 발생하였습니다");
}
putBookAndAuthor()에 처음에 public을 안붙이니.. 롤백이 되지 않았음..
-> 이유는 차차 알아가보자.., default로 처리되어있을때 jpa 내부 처리 방식에 오류가 생긴듯
둘중 어디에 throw new RuntimeException을 해도 롤백이 진행됨
putAuthor에 RuntimeException 붙인 경우
-> putBookAndAuthor만 실행하였을 때는 예외처리를 하였기에 putAuthor(), save(book) 둘다 정상 로직으로 처리되었지만 롤백됨
-> putAuthor() 메서드에 접근전 putBookAndAuthor()에서 자체적으로 트랜잭션을 생성
하였고, putAuthor()는 트랜잭션을 재활용하였는데 여기서 에러가 발생하였기에 둘다 성공,실패 되어야 한다는 ACID에 근거해 롤백
됨
트랜잭션이 있던 없던 상관없이, 새로운 트랜잭션을 만들어 독립(자체)적으로 커밋과 롤백을 진행
위에서 설명한 putAuthor에 RuntimeException 붙인 경우
에서
putAuthor()에만 REQUIRES_NEW를 붙이게 되면 Author만 롤백 되는 것을 볼 수 있음
반대로 PutBookAndAuthor에만 RuntimeException을 붙인 경우
에는
Book만 롤백 됨!
별도의 트랜잭션을 생성하지 않음, 하나의 트랜잭션이지만 분리되어 동작
-> 종속적이지만 상위 코드에는 영향 X
save point (중간 저장) 까지의 성공은 보장
-> but JPA에서는 NESTED(기본 옵션 사용시
) 전파를 사용하지 못함 (의도와는 다른 결과 때문)
트랜잭션이 있는 경우 그 트랜잭션을 사용, 없는 경우 트랜잭션 사용X (새로 안만듬)
트랜잭션 없이 별개로 동작, 다른 트랜잭션이 수행이 된 후 실행
필수적으로 트랜잭션이 반드시 존재해야 함, 트랜잭션이 없으면 오류 발생
트랜잭션이 없어야 함, 트랜잭션이 있는 경우 오류 발생