
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 처리XUnChecked 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 (새로 안만듬)
트랜잭션 없이 별개로 동작, 다른 트랜잭션이 수행이 된 후 실행
필수적으로 트랜잭션이 반드시 존재해야 함, 트랜잭션이 없으면 오류 발생
트랜잭션이 없어야 함, 트랜잭션이 있는 경우 오류 발생