Reference. 한 번에 끝내는 Java/Spring 웹 개발 마스터 초격차 패키지 Online
이전 글
더보기
1. 트랜잭션
- 트랜잭션 ACID(원자성, 일관성, 고립성, 지속성)
- 원자성 (Atomicity)
- 작업들이 중간에 중단되어도 일관성 보장을 의미 (모두 성공 or 실패)
- 일관성 (Consistency)
- 데이터간에 정합성을 맞추는 의미 (데이터의 일관성 있는 상태로 유지)
- 독립성 (Isolation)
- 트랜잭션 수행 시 다른 트랜잭션의 연산을 못하도록 보장
- 고립성이라고도 말하며, 성능 관련 이유로 가장 유연성 있는 제약조건
- 귀속성 (Durability)
- 성공적으로 수행된 트랜잭션은 영원히 반영 (데이터 영구 보관)
- 원자성 (Atomicity)
@Transactional
- 2가지 라이브러리의 차이
javax.transaction
: 스프링에 의존없이 사용 가능 (다른 컨테이너 사용 가능)
org.springframework.transactional.annotaion
: 스프링에 많은 기능 사용 가능
- 메소드, 클래스(포함된 모든 메소드 적용)에 사용 가능 (우선순위는 메소드)
- 블럭을 기준으로 시작시점에 트랜잭션 시작, 종료시점에 트랜잭션 종료
//메소드에서 실행한 경우 @Transactional public void pubBookAndAuthor(){ Book book = new Book(); book.setName("JPA 시작하기"); bookRepository.save(book); Author author = new Author(); author.setName("martin"); authorRepository.save(author); }
- 2가지 라이브러리의 차이
1.1. 테스트 코드 작성
BookServiceTest.java
@SpringBootTest
class BookServiceTest {
private final BookService bookService;
private final BookRepository bookRepository;
private final AuthorRepository authorRepository;
@Autowired
public BookServiceTest(BookService bookService, BookRepository bookRepository, AuthorRepository authorRepository){
this.bookService = bookService;
this.bookRepository = bookRepository;
this.authorRepository = authorRepository;
}
@Test
void transactionTest(){
//RuntimeException이 발생해도 조회가 가능하기 위해 임시처리
try {
bookService.pubBookAndAuthor();
} catch (RuntimeException e) {
System.out.println(">>> " + e.getMessage());
}
System.out.println("books : " + bookRepository.findAll());
System.out.println("authors : " + authorRepository.findAll());
}
}
1. 트랜잭션이 없는 경우 (@Transactional 주석처리)
- Exception이 발생해도 메소드 블럭에
@Transactional
이 없어 데이터 반영 처리
JpaRepository
save()
안에 트랜잭션이 존재하므로save()
마다 트랜잭션 처리
BookService.java
@Service
@RequiredArgsConstructor
public class BookService {
private final BookRepository bookRepository;
private final AuthorRepository authorRepository;
// @Transactional
public void pubBookAndAuthor(){
Book book = new Book();
book.setName("JPA 시작하기");
bookRepository.save(book);
Author author = new Author();
author.setName("martin");
authorRepository.save(author);
throw new RuntimeException("오류 발생 commit 실패");
}
}
실행결과
...
books : [Book(super=BaseEntity(createdAt=2021-08-07T23:34:19.368267, updatedAt=2021-08-07T23:34:19.368267), id=1, name=JPA 시작하기, category=null)]
...
authors : [Author(super=BaseEntity(createdAt=2021-08-07T23:34:19.591270, updatedAt=2021-08-07T23:34:19.591270), id=1, name=martin, country=null)]
2. 트랜잭션이 있는 경우
@Transactional
이 메소드에 선언되어 블럭에Exception
이 발생하여rollback
처리
BookService.java (pubBookAndAuthor)
@Transactional
public void pubBookAndAuthor(){
Book book = new Book();
book.setName("JPA 시작하기");
bookRepository.save(book);
Author author = new Author();
author.setName("martin");
authorRepository.save(author);
throw new RuntimeException("오류 발생 commit 실패");
}
실행결과
...
books : []
...
authors : []
1.2. 트랜잭션에 잘못된 대표 사례
Checked Exception
에 사용 (Checked - UnChecked
에 혼용이 원인)Checked Exception
- 대표 Class :
Exception
- 명시적인
Exception
처리가 필요
- 예외가 발생해도 트랜잭션이
Rollback
처리X
- 개발자가
Exception catch
에서 트랜잭션을 핸들링하도록 가이드
- 대표 Class :
UnChecked Exception
- 대표 Class :
RuntimeException
- 예외가 발생하면
Rollback
처리O
- 대표 Class :
Checked Exception
과UnChecked Exception
차이가 발생하는 이유UnChecked Exception
처리
//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; }
//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)) { ...
//DefaultTransactionAttribute.java @Override public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); //RuntimeException or Error만 롤백 }
Checked Exception
처리
//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 { ... } } //테스트 코드 실행 결과 books : [] authors : []
- 메소드에 참조하는 메소드가
@Transaction
인 경우 (해당 메소드는 @Transaction X)- 스프링 컨테이너는 빈으로 진입할 때 메소드에 걸려있는 어노테이션에 대해서만 처리
put()
에는@Transactional
이 존재하지 않으므로 처리X
... public class BookService { ... public void put(){ this.pubBookAndAuthor(); } @Transactional void pubBookAndAuthor(){ ... throw new RuntimeException("오류 발생 commit 실패"); } } //테스트 실행 결과 (Rollback 실패) >>> 오류 발생 commit 실패 books : [Book(super=BaseEntity(createdAt=2021-08-08T00:29:26.443095, updatedAt=2021-08-08T00:29:26.443095), id=1, name=JPA 시작하기, category=null)] authors : [Author(super=BaseEntity(createdAt=2021-08-08T00:29:26.598097, updatedAt=2021-08-08T00:29:26.598097), id=1, name=martin, country=null)]
1.3. 스프링 @Transactional 어노테이션 기능
1. isolation()
(격리 수준)
- 트랜잭션 격리단계를 의미, 트랜잭션간에 데이터 접근을 어떤식으로 정의할 것인지 설정
- 격리 수준:
DEFAULT
,READ_UNCOMMITTED
,READ_COMMITTED
,REPEATABLE_READ
,SERIALIZABLE
- 일반적으로
READ_COMMITTED
,REPEATABLE_READ
를 많이 사용(정합성과 성능상의 이유)
DEFAULT
: 데이터베이스에 격리 단계를 사용(MySQL
default:REPATABLE_READ
)
- 레벨이 높을 수록 데이터정합성은 정확해지고 성능향상은 저하 (
DEFAULT
는 DB설정단계 사용)READ_UNCOMMITTED
: 레벨0, 다른 트랜잭션 수행에 커밋되지 않은 결과를 조회(Dirty Read)
READ_COMMITTED
: 레벨1, 다른 트랜잭션에서 커밋된 결과를 조회 (Unrepeatable 상태)
REPEATABLE_READ
: 레벨2
SERIALIZABLE
: 레벨3
-isolation.READ_UNCOMMITTED
테스트 실행 및 결과
- IDE debug모드를 사용하여 브레이크 포인트를 지정 후 데이터베이스엔 수정 쿼리 실행
READ_UNCOMMITTED
는 실행중인 트랜잭션에서 다른 트랜잭션 결과에 영향을 받음
- 데이터 조회(find) 테스트
- 자바 트랜잭션을 이용한 테스트 코드 작성 (
BookServiceTest.java
)@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()); }
- 데이터베이스에 쿼리를 이용한 트랜잭션 생성
start transaction; update book set category='none'; commit;
- 결과 확인
//실행중인 트랜잭션이 완료되지 않은 상태(브레이크 포인트에 걸려있는 상태) //데이터베이스를 이용한 쿼리가 커밋되지 않았는데 결과가 java 트랜잭션 반영 >>> Optional[Book(super=BaseEntity(createdAt=2021-08-08T09:02:45.370835, updatedAt=2021-08-08T09:02:45.370835), id=1, name=JPA 강의, category=none)]
- 자바 트랜잭션을 이용한 테스트 코드 작성 (
- 데이터 수정(save) 테스트
- 자바 트랜잭션을 이용한 테스트 코드 작성 (
BookServiceTest.java
)@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); }
- 데이터베이스에 쿼리를 이용한 트랜잭션 생성
start transaction; update book set category='none'; commit;
- 결과 확인
// 1 브레이크 포인트에서 데이터베이스 트랜잭션 시작 후 update 실행 // 트랜잭션 락이 발생, 데이터베이스에서 commit/rollback 실행하면 락이 해제 // commit과 rollback이 아래와 같은 동일한 결과를 반환 // 이유는 jpa에서 commit되지 않은 값을 가지고 있다가 save에서 모두 반영하기 때문 >>> [Book(super=BaseEntity(createdAt=2021-08-08T09:27:29.000685, updatedAt=2021-08-08T09:27:44.300163), id=1, name=바꾼이름, category=none)]
- JPA에서
Dirty Read
를save
처리 방법 (임시 방편, 데이터 정합성 해결X)Book.java
에@DynamicUpdate
를 추가- JPA에서 수정하고자 하는 컬럼(
name
)만 반영 됌 (book.setName("바꾼이름");
)
... @DynamicUpdate public class Book extends BaseEntity{ ...
Hibernate: update book set updated_at=?, name=? where id=? >>> [Book(super=BaseEntity(createdAt=2021-08-08T09:35:31.706323, updatedAt=2021-08-08T09:35:53.869175), id=1, name=바꾼이름, category=null)]
- JPA에서 수정하고자 하는 컬럼(
- 자바 트랜잭션을 이용한 테스트 코드 작성 (
-isolation.READ_COMMITTED
테스트 실행 및 결과
isolation.READ_UNCOMMITTED
를 해결하기 위해 등장, 테스트 방법은READ_UNCOMMITTED
와 동일
BookService.java
에@Transactional(isolation = Isolation.READ_COMMITTED)
로 수정
Book.java
에@DynamicUpdate
를 제거 (커밋된 것만 조회하므로 필요 없음)
- 실행결과
//DynamicUpdate를 제거했기 때문에 모든 컬럼에 대해 update Hibernate: update book set updated_at=?, category=?, name=?, publisher_id=? where id=? //Dirty Read 현상 제거 >>> [Book(super=BaseEntity(createdAt=2021-08-08T09:43:48.675890, updatedAt=2021-08-08T09:43:57.891441), id=1, name=바꾼이름, category=null)]
READ_COMMITTED
에 문제점- 1 브레이크 위치에서 다른 트랜잭션 수행에 commit이 발생하면 2 브레이크 결과가 달라짐
- 첫번째 브레이크
category = null
, 두번째 브레이크category = none
@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() 실행 }
//실행결과 //2 브레이크에서 데이터베이스 update 후 commit //1 브레이크 조회에서는 commit된 것이 없기 때문에 category = null >>> Optional[Book(super=BaseEntity(createdAt=2021-08-08T10:04:39.933340, updatedAt=2021-08-08T10:04:39.933340), id=1, name=JPA 강의, category=null)] //2 브레이크 조회에서는 update commit이 진행되어 category=none >>> Optional[Book(super=BaseEntity(createdAt=2021-08-08T10:04:39.933340, updatedAt=2021-08-08T10:04:39.933340), id=1, name=JPA 강의, category=none)]
- 첫번째 브레이크
- 1 브레이크 위치에서 다른 트랜잭션 수행에 commit이 발생하면 2 브레이크 결과가 달라짐
-isolation.REPEATABLE_READ
테스트 실행 및 결과
isolation.READ_COMMITTED
를 해결하기 위해 등장, 테스트 소스 및 구성은 동일
- 조회결과를 별도의 스냅샷으로 저장 트랜잭션이 종료되기 전까지는 그 값을 리턴
- 수행중인 트랜잭션안에서만 영향을 주고 받음
BookService.java
에@Transactional(isolation = Isolation.REPEATABLE_READ)
로 수정
REPEATABLE_READ
문제점팬텀 리드 (Phantom read)
: 트랜잭션 내에서 같은 쿼리를 실행하지만 예상결과와 다른 결과가 나오는 현상이 발생
- 1 브레이크에서 데이터베이스르 insert문을 commit
- 트랜잭션내 실행으로 1개의 데이터만 수정 될 것처럼 보이지만 안보이던 insert문도 수정
//BookRepository.java @Repository public interface BookRepository extends JpaRepository<Book, Long> { @Modifying @Query(value = "update book set category='none'", nativeQuery = true) void update(); }
//BookService.java @Transactional(isolation = Isolation.REPEATABLE_READ) public void get(Long id){ ... bookRepository.update(); // 1 브레이크 entityManager.clear(); }
//데이터베이스 쿼리 start transaction; insert into book(id, name) values(2, 'jpa 강의 2'); commit;
-isolation.SERIALIZABLE
테스트 실행 및 결과
isolation.REPEATABLE_READ
를 해결하기 위해 등장, 테스트 소스 및 구성은 동일
- 다른 트랜잭션이 끝날때 까지 무조건 기다리고 처리가 완료되면 실행 (데이터 정합성 100%)
- 웨이팅이 길어져서 성능에는 안좋은 영향 발생
2. propagation()
(트랜잭션 전파)
- 실습을 위한 공통 테스트 코드
@SpringBootTest
class BookServiceTest {
@Autowired
private final BookService bookService;
@Autowired
private final BookRepository bookRepository;
@Autowired
private final AuthorRepository authorRepository;
@Autowired
private final EntityManager entityManager;
@Test
void transactionTest(){
try {
bookService.pubBookAndAuthor();
} catch (RuntimeException e) {
System.out.println(">>> " + e.getMessage());
}
System.out.println("books : " + bookRepository.findAll());
System.out.println("authors : " + authorRepository.findAll());
}
}
- Spring
@Transactional
은Propagation.java
의 7가지 설정 지원 (default: REQIRED
)REQUIRED
:- 기존에 사용하는 트랜잭션이 있으면 그것을 사용, 없으면 새로운 트랜잭션 생성
- JPA Repository에
save()
가REQUIRED
전파를 사용 (코드블럭 내에선 동일 트랜잭션)
UnChecked Exception
이 일어나면 전파된 트랜잭션 모두rollback
@Service @RequiredArgsConstructor public class AuthorService { private final AuthorRepository authorRepository; @Transactional(propagation = Propagation.REQUIRED) public void putAuthor() { Author author = new Author(); author.setName("martin"); authorRepository.save(author); throw new RuntimeException("오류가 발생"); //여기서 발생해도 롤백 } }
@Transactional(propagation = Propagation.REQUIRED) public void pubBookAndAuthor(){ Book book = new Book(); book.setName("JPA 시작하기"); bookRepository.save(book); //오류가 전파되지 않도록 try - catch를 사용 try{ authorService.putAuthor(); }catch (RuntimeException e){ } throw new RuntimeException("오류가 발생"); //여기서 발생해도 롤백 }
//실행결과 books : [] authors : []
REQUIRES_NEW
:- 기존 트랜잭션 유무와는 상관없이 무조건 새로운 트랜잭션을 생성
@Transactional(propagation = Propagation.REQUIRES_NEW) public void putAuthor() { ... throw new RuntimeException("오류가 발생"); //여기서 롤백 }
@Transactional(propagation = Propagation.REQUIRED) public void pubBookAndAuthor(){ ... }
//실행 결과 books : [Book(super=BaseEntity(createdAt=2021-08-08T14:05:27.011909, updatedAt=2021-08-08T14:05:27.011909), id=1, name=JPA 시작하기, category=null)] authors : []
NESTED
:- 별도의 트랜잭션을 생성하지 않음, 하나의 트랜잭션이지만 분리되어 동작
- save point (중간 저장) 까지는 보장
- JPA에서는
NESTED
전파를 사용하지 못함 (의도와는 다른 결과 때문)
SUPPORTS
:- 트랜잭션이 있는 경우 그 트랜잭션을 사용, 없는 경우 트랜잭션 사용X (새로 안만듬)
NOT_SUPPORTED
:- 트랜잭션 없이 별개로 동작, 다른 트랜잭션이 수행이 된 후 실행
MANDATORY
:- 필수적으로 트랜잭션이 반드시 존재해야 함, 트랜잭션이 없으면 오류 발생
NEVER
:- 트랜잭션이 없어야 함, 트랜잭션이 있는 경우 오류 발생
'백엔드 > JPA' 카테고리의 다른 글
[ JPA ] 8. 커스텀 쿼리 사용 (0) | 2021.08.18 |
---|---|
[ JPA ] 7. 영속성 전이 (Cascade) (0) | 2021.08.12 |
[ JPA ] 6-3. Entity 생명주기 (1) | 2021.08.08 |
[ JPA ] 6-2. Entity 캐시 (0) | 2021.08.07 |
[ JPA ] 6-1. 영속성 컨텍스트(Persistence Context) (0) | 2021.08.07 |