Spring
JPA의 Transactional
옴악핫세
2023. 5. 15. 06:14
JPA의 트랜잭션
트랜잭션이란?
간단하게 말해서 아래의 질의어(SQL)를 이용하여 데이터베이스를 접근 하는 것을 의미한다.
- SELECT
- INSERT
- DELETE
- UPDATE
착각하지 말아야 할 것은, 작업의 단위는 질의어 한문장이 아니라는 점이다.
이때, 모든 SQL이 성공적으로 수행이 되면 DB에 영구적으로 변경을 반영하지만 SQL 중 단 하나라도 실패한다면 모든 변경을 되돌립니다.
- JPA는 DB의 이러한 트랜잭션 개념을 사용하여 Entity를 관리할 수 있습니다.
영속성 컨텍스트의 트랜잭션
- 영속성 컨텍스트에 Entity 객체들을 담아 관리한다고 해서 DB에 바로 반영 되지는 않습니다.
- DB에서 하나의 트랜잭션에 여러 개의 SQL을 포함하고 있다가 마지막에 영구적으로 변경을 반영하는 것 처럼 JPA에서도 영속성 컨텍스트로 관리하고 있는 변경이 발생한 객체들의 정보를 토대로 SQL을 만들어 전부 가지고 있다가 마지막에 변경을 반영합니다.
- EntityTransaction 성공 테스트
@Test
@DisplayName("EntityTransaction 테스트")
void transactionTest() {
EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져옵니다.
et.begin(); // 트랜잭션을 시작합니다.
try { // DB 작업을 수행합니다.
Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
memo.setId(1L); // 식별자 값을 넣어줍니다.
memo.setUsername("Robbie");
memo.setContents("영속성 컨텍스트와 트랜잭션 이해하기");
em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.
et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit 을 호출합니다.
// commit 이 호출되면서 DB 에 수행한 DB 작업들이 반영됩니다.
} catch (Exception ex) {
ex.printStackTrace();
et.rollback(); // DB 작업 중 오류 발생 시 rollback 을 호출합니다.
} finally {
em.close(); // 사용한 EntityManager 를 종료합니다.
}
emf.close(); // 사용한 EntityManagerFactory 를 종료합니다.
}
- JPA에서 이러한 트랜잭션의 개념을 적용하기 위해서는 EntityManager에서 EntityTransaction을 가져와 트랜잭션을 적용하면 됩니다.
- EntityTransaction et = em.getTransaction();
- 해당 코드를 호출하여 EntityTransaction을 가져와 트랜잭션을 관리할 수 있습니다.
- et.begin();
- 트랜잭션을 시작하는 명령어입니다.
- et.commit();
- 트랜잭션의 작업들을 영구적으로 DB에 반영하는 명령어입니다.
- et.rollback();
- 오류가 발생했을 때 트랜잭션의 작업을 모두 취소하고, 이전 상태로 되돌리는 명령어입니다.
- EntityTransaction 실패 테스트.
@Test
@DisplayName("EntityTransaction 실패 테스트")
void test2() {
EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져옵니다.
et.begin(); // 트랜잭션을 시작합니다.
try { // DB 작업을 수행합니다.
Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
memo.setUsername("Robbie");
memo.setContents("실패 케이스");
em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.
et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit 을 호출합니다.
// commit 이 호출되면서 DB 에 수행한 DB 작업들이 반영됩니다.
} catch (Exception ex) {
System.out.println("식별자 값을 넣어주지 않아 오류가 발생했습니다.");
ex.printStackTrace();
et.rollback(); // DB 작업 중 오류 발생 시 rollback 을 호출합니다.
} finally {
em.close(); // 사용한 EntityManager 를 종료합니다.
}
emf.close(); // 사용한 EntityManagerFactory 를 종료합니다.
}
- 식별자 값을 넣어주지 않아 오류가 발생했습니다.
- 따라서 et.rollback(); 코드가 호출이되어 트랜잭션 작업 내용들이 취소되었습니다.
- DB를 확인해보면 해당 작업이 반영되어있지 않은 것을 확인할 수 있습니다.
@Transactional 이해하기
- @Transactional
- Spring 프레임워크에서는 DB의 트랜잭션 개념을 애플리케이션에서 적용할 수 있도록 트랜잭션 관리자를 제공합니다.
@Transactional(readOnly = true) public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> { ... @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 애너테이션을 클래스나 메서드에 추가하면 쉽게 트랜잭션 개념을 적용할 수 있습니다.
- 메서드가 호출되면, 해당 메서드 내에서 수행되는 모든 DB 연산 내용은 하나의 트랜잭션으로 묶입니다.
- 이때, 해당 메서드가 정상적으로 수행되면 트랜잭션을 커밋하고, 예외가 발생하면 롤백합니다.
- 클래스에 선언한 @Transactional 해당 클래스 내부의 모든 메서드에 트랜잭션 기능을 부여합니다.
- 이때, save 메서드에 @Transactional 애너테이션이 추가되어있기 때문에 readOnly = true 옵션인 @Transactional을 덮어쓰게 되어 readOnly = false 옵션으로 변경됩니다.
- readOnly 옵션
- 트랜잭션에서 데이터를 읽기만 할 때 사용됩니다.
- 이 속성을 사용하면 읽기 작업에 대한 최적화를 수행할 수 있습니다.
- 해당 트랜잭션에서 데이터를 수정하려고 하면 예외가 발생합니다.
- @Transactional 테스트
- SpringBoot 환경에서는 EntityManagerFactory와 EntityManager를 자동으로 생성해줍니다.
- application.properties에 DB 정보를 전달해 주면 이를 토대로 EntityManagerFactory가 생성됩니다.
@PersistenceContext EntityManager em;
- @PersistenceConext 애너테이션을 사용하면 자동으로 생성된 EntityManager를 주입받아 사용할 수 있습니다.
- 트랜잭션이 적용되어 DB 작업이 성공했습니다.
@Test @DisplayName("메모 생성 실패") void test2() { Memo memo = new Memo(); memo.setUsername("Robbie"); memo.setContents("@Transactional 테스트 중!"); assertThrows(TransactionRequiredException.class, () -> { // 발생되는 오류를 테스트 할 수 있습니다. em.persist(memo); // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다. }); }
- 트랜잭션이 적용되지 못해 작업이 취소되었습니다.
- SpringBoot 환경에서는 EntityManagerFactory와 EntityManager를 자동으로 생성해줍니다.
@Test
@DisplayName("메모 생성 실패")
void test2() {
Memo memo = new Memo();
memo.setUsername("Robbie");
memo.setContents("@Transactional 테스트 중!");
assertThrows(TransactionRequiredException.class, () -> { // 발생되는 오류를 테스트 할 수 있습니다.
em.persist(memo); // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
});
}
- SpringBoot 환경에서의 JPA
- 영속성 컨텍스트와 트랜잭션의 생명주기
- 스프링 컨테이너 환경에서는 영속성 컨텍스트와 트랜잭션의 생명주기가 일치합니다.
@Repository
@Transactional(readOnly = true)
public class MemoRepository {
@Transactional
public Memo createMemo(EntityManager em) {
Memo memo = new Memo();
memo.setUsername("Robbin");
memo.setContents("영속성 컨텍스트와 트랜잭션의 생명주기 테스트 중!");
em.persist(memo);
System.out.println(" @Transactional 적용 상태의 memo = " + em.contains(memo)); // 영속성 컨텍스트에 저장된 상태인지 확인합니다.
return memo;
}
}
@Test
@DisplayName("영속성 컨텍스트와 트랜잭션의 생명 주기")
void test3() {
Memo memo = memoRepository.createMemo(em);// 트랜잭션이 적용되어있는 createMemo 메서드를 호출합니다.
System.out.println(" @Transactional 비적용 상태의 memo = " + em.contains(memo)); // 영속성 컨텍스트에 저장된 상태인지 확인합니다.
}
- @Transactional 속성
- isolation
- 트랜잭션 격리 수준을 설정하는 데 사용됩니다.
- 격리 수준의 종류
- DEFAULT : 데이터베이스 기본 격리 수준을 사용합니다.
- 대부분의 데이터베이스 에서는 READ_COMMITTED 격리 수준을 기본으로 사용합니다.
- READ_UNCOMMITTED : 커밋되지 않은 데이터를 읽을 수 있습니다.
- 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽을 수 있기 때문에, 이 격리 수준에서는 DirtyRead 문제가 발생할 수 있습니다.
- READ_COMMITTED : 커밋된 데이터만 읽을 수 있습니다.
- 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽지 않으므로 Dirty Read 문제는 발생하지 않지만, Non-Repeatable Read 문제가 발생할 수 있습니다.
- REPEATABLE_READ : 같은 쿼리를 실행해도 결과가 항상 동일합니다.
- Non-Repeatable Read 문제는 발생하지 않지만, Phantom Read 문제가 발생할 수 있습니다.
- SERIALIZABLE : 모든 트랜잭션을 순차적으로 실행합니다.
- Dirty Read, Non-Repeatable Read, Phantom Read 문제는 발생하지 않지만, 성능이 매우 저하될 수 있습니다.
- DEFAULT : 데이터베이스 기본 격리 수준을 사용합니다.
- propagation
- **@Transactional**애노테이션이 있는 메서드에서 이미 시작된 트랜잭션이 있을 경우 이 트랜잭션을 사용할지, 새로운 트랜잭션을 시작할지를 지정합니다.
- 종류
- REQUIRED : 이미 시작된 트랜잭션이 있으면 해당 트랜잭션을 사용하고, 없으면 새로운 트랜잭션을 시작합니다. 기본값입니다.
- 중간에 자식/부모에서 롤백이 발생된다면 자식과 부모 모두 롤백 합니다.
- REQUIRES_NEW : 항상 새로운 트랜잭션을 시작합니다. 이미 시작된 트랜잭션은 일시 중단됩니다.
- NESTED 방식으로 메서드 호출이 이루어지더라도 롤백은 각각 이루어 집니다.
- SUPPORTS : 이미 시작된 트랜잭션이 있으면 해당 트랜잭션을 사용하고, 없으면 트랜잭션 없이 실행합니다.
- MANDATORY : 이미 시작된 트랜잭션이 있으면 해당 트랜잭션을 사용하고, 없으면 예외를 던집니다.
- NOT_SUPPORTED : 트랜잭션 없이 실행합니다. 이미 시작된 트랜잭션은 일시 중단됩니다.
- NEVER : 트랜잭션 없이 실행합니다. 이미 시작된 트랜잭션이 있으면 예외를 던집니다.
- NESTED : 부모 트랜잭션이 존재하면 부모 트랜잭션에 중첩시키고, 부모 트랜잭션이 존재하지 않는다면 새로운 트랜잭션을 생성합니다.
- 부모 트랜잭션에 예외가 발생하면 자식 트랜잭션도 롤백 합니다.
- 자식 트랜잭션에 예외가 발생하더라도 부모 트랜잭션은 롤백하지 않습니다.
- 롤백은 부모 트랜잭션에서 자식 트랜잭션을 호출하는 지점까지만 롤백 됩니다. 이후 부모 트랜잭션에서 문제가 없으면 부모 트랜잭션은 끝까지 commit 됩니다.
- REQUIRED : 이미 시작된 트랜잭션이 있으면 해당 트랜잭션을 사용하고, 없으면 새로운 트랜잭션을 시작합니다. 기본값입니다.
- readOnly
- 트랜잭션에서 데이터를 읽기만 할 때 사용됩니다.
- 이 속성을 사용하면 읽기 작업에 대한 최적화를 수행할 수 있습니다.
- 해당 트랜잭션에서 데이터를 수정하려고 하면 예외가 발생합니다.
- rollbackFor
- 특정 예외가 발생했을 때 트랜잭션을 롤백하는데 사용됩니다.
@Transactional(rollbackFor = Exception.class) public void Method() { // 트랜잭션 처리 코드 }
- 모든 예외가 발생하면 트랜잭션을 롤백하도록 설정할 수 있습니다.
- noRollbackFor
- 특정 예외가 발생했을 때 트랜잭션을 롤백하는데 사용됩니다.
@Transactional(noRollbackFor = {IOException.class, TimeoutException.class}) public void Method() { // 트랜잭션 처리 코드 }
- **IOException.class**와 **TimeoutException.class**를 지정하여 이 두 예외가 발생해도 트랜잭션을 롤백하지 않도록 설정할 수 있습니다.
- isolation
트랜잭션 깊게 이해하기
- 트랜잭션의 성질(ACID)
- 트랜잭션은 네 가지의 성질인 ACID를 가지고 있습니다.
- 트랜잭션은 작업의 일부분이라도 실패할 경우 전체 작업이 취소됩니다.
- 즉, 모든 쿼리문이 성공적으로 수행 되어야만 데이터베이스에 변경이 반영됩니다.
- 이렇게 함으로써 데이터의 무결성을 보장할 수 있습니다.
- 트랜잭션이 수행되기 전과 수행된 후에도 데이터베이스가 일관성 있는 상태를 유지해야 합니다.
- 즉, 트랜잭션 전후에 데이터베이스의 제약 조건이 만족되어야 합니다.
- 트랜잭션은 다른 트랜잭션의 작업에 영향을 받지 않고 독립적으로 수행되어야 합니다.
- 여러 개의 트랜잭션이 동시에 수행될 때, 각각의 트랜잭션은 서로를 모르고 자신만의 데이터베이스를 수정하는 것처럼 동작해야 합니다.
- 트랜잭션이 성공적으로 수행된 후에는 영구적인 데이터베이스의 변경을 보장해야 합니다.
- 즉, 시스템이 장애가 발생하더라도 영구적으로 저장된 데이터는 손실되지 않아야 합니다.
- 트랜잭션의 상태
- 트랜잭션은 여러 단계를 거쳐 실행됩니다. 이러한 단계를 트랜잭션의 상태라고 합니다.
- 트랜잭션이 실행 중인 상태를 말합니다.
- 트랜잭션 실행 중 오류가 발생하여 작업이 실패한 상태를 말합니다.
- 트랜잭션의 모든 쿼리문이 성공적으로 수행되었지만, 아직 커밋되지 않은 상태를 말합니다.
- 트랜잭션의 모든 쿼리문이 성공적으로 수행되었고, 커밋된 상태를 말합니다.
- 트랜잭션이 롤백되어 중단된 상태를 말합니다.
- 트랜잭션의 제어
- 트랜잭션을 제어하는 명령어는 크게 커밋(Commit)과 롤백(Rollback)이 있습니다.
- 트랜잭션의 작업을 영구적으로 데이터베이스에 반영하는 명령어입니다.
- 트랜잭션의 작업을 모두 취소하고, 이전 상태로 되돌리는 명령어입니다.
- 트랜잭션의 격리 수준(Isolation Level)
- 트랜잭션 격리 수준은 여러 개의 트랜잭션이 동시에 수행될 때, 각각의 트랜잭션이 다른 트랜잭션의 변경 작업을 어느 정도까지 알아볼 수 있는지를 결정합니다.
- 격리 수준이 높을수록 데이터베이스의 일관성과 무결성을 보장할 수 있지만, 동시성이 떨어지는 단점이 있습니다.
- 트랜잭션 격리 수준에는 다음과 같은 종류가 있습니다.
- 격리 수준****(Isolation Level)****
- 다른 트랜잭션에서 커밋되지 않은 데이터 변경 내용을 읽을 수 있습니다.
- 따라서 Dirty Read 문제가 발생할 가능성이 있습니다.
- 가장 낮은 격리 수준이기 때문에 동시성은 가장 높지만 일관성과 무결성은 보장되지 않습니다.
- 트랜잭션에서 변경 내용이 커밋 되어야 값을 읽을 수 있습니다.
- 한 트랜잭션에서 변경한 데이터를 다른 트랜잭션이 읽을 수 없고, 커밋된 데이터만 읽을 수 있습니다.
- 즉, 다른 트랜잭션에서 수정 중인 데이터를 읽을 수 없습니다.
- 따라서 Dirty Read 문제는 발생하지 않지만, Non-Repeatable Read 문제가 발생할 수 있습니다.
- 트랜잭션 내에서 같은 쿼리를 실행하면 항상 같은 결과가 반환되도록 보장합니다.
- 트랜잭션에서 읽은 데이터에 대해 공유잠금(shared lock)을 걸어 다른 트랜잭션에서 변경하지 못하도록 합니다.
- 일반적으로 동시성을 보장하면서 일관성과 무결성을 보장하기 위해 사용되는 격리 수준입니다.
- Phantom Read 문제가 발생할 수 있습니다.
- 대용량 트랜잭션 처리 시 너무 많은 로우 락을 유발하여 성능 저하를 야기할 수 있습니다.
- 최고 수준의 격리 수준입니다.
- 한 트랜잭션이 완료될 때까지 다른 트랜잭션에서 해당 데이터를 변경할 수 없습니다.
- 모든 읽기와 쓰기 작업이 테이블 단위 락을 획득하여 직렬화되어 실행됩니다.
- 이러한 락 때문에 다른 트랜잭션에서는 해당 데이터에 대한 작업을 수행할 수 없으므로 동시성이 제한됩니다.
- 따라서 동시성이 제일 떨어지는 격리 수준입니다.
- Phantom Read, Non-Repeatable Read 문제는 발생하지 않습니다.
- 일반적으로 극히 드물게 사용됩니다.
- Dirty Read
- Dirty Read는 어떤 트랜잭션이 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽어들이는 현상을 말합니다.
- Dirty Read가 발생하면, 데이터 일관성이 깨질 수 있으므로 데이터베이스에서는 이를 방지하기 위해 트랜잭션 격리 수준을 제공합니다.
- 격리 수준이 높을수록 데이터의 일관성은 보장되지만, 동시성 처리 성능은 저하될 수 있습니다.
- READ UNCOMMITTED 격리 수준에서 발생할 수 있습니다.
- Non-Repeatable Read
- Non-Repeatable Read는 트랜잭션이 동일한 데이터를 두 번 이상 실행할 때 결과 값이 다른 문제를 말합니다. 이 문제는 트랜잭션 동안 데이터가 변경될 경우 발생할 수 있습니다.
- READ COMMITTED 격리 수준에서 발생할 수 있습니다.
- Phantom Read
- 트랜잭션에서 동일한 쿼리를 실행 했을 때, 이전에는 없었던 새로운 레코드가 결과에 포함되는 현상을 의미합니다.
- Non-Repeatable Read 와 Phantom Read 비교
- Non-Repeatable Read는 하나의 데이터에 대한 문제이고, Phantom Read는 결과 집합에 대한 문제입니다.
- 다른 트랜잭션에서 커밋되지 않은 데이터 변경 내용을 읽을 수 있습니다.
트랜잭션 전파
- @Transactional propagation 이해하기
- REQUIRED
- testRequired()
@Transactional public void testRequired() throws RuntimeException { try { User user1 = new User("robbie1", "1234", "required1"); userRepository.save(user1); testChildService.testRequiredChild(); User user3 = new User("robbie3", "1234", "required3"); userRepository.save(user3); } catch (RuntimeException ex) { ex.printStackTrace(); } }
- testRequiredChild()
@Transactional public void testRequiredChild() throws RuntimeException { User user2 = new User("robbie2", "1234", "required2"); userRepository.save(user2); throw new RuntimeException(); }
- 부모 메서드인 testRequired()에 트랜잭션이 존재하기 때문에 자식 메서드인 testRequiredChild()는 부모 트랜잭션에 합류합니다.
- 따라서 예외처리를 하더라도 자식 메서드에서 RuntimeException 오류가 발생하면
- Transaction silently rolled back because it has been marked as rollback-only
- 위 문구가 나오면서 실행된 insert 쿼리 2개는 롤백됩니다.
- DB를 확인해보면 User는 저장 되어있지 않습니다.
- REQUIRES_NEW
- testRequiredNew()
@Transactional public void testRequiredNew() { try { User user1 = new User("robbie1", "1234", "requiredNew1"); userRepository.save(user1); testChildService.testRequiredNewChild(); User user3 = new User("robbie3", "1234", "requiredNew3"); userRepository.save(user3); } catch (RuntimeException ex) { ex.printStackTrace(); } }
- testRequiredNewChild()
@Transactional(propagation = Propagation.REQUIRES_NEW) public void testRequiredNewChild() { User user2 = new User("robbie2", "1234", "requiredNew2"); userRepository.save(user2); throw new RuntimeException(); }
- 부모 메서드인 testRequiredNew()에 트랜잭션이 존재해도 자식 메서드인 testRequiredNewChild()는 무조건 새로운 트랜잭션을 만듭니다.
- 따라서 서로 다른 트랜잭션이기 때문에 오류가 발생하기 전 user1을 저장하는 insert 쿼리는 커밋이 됩니다.
- DB를 확인해 보면 nickname: requiredNew1 이 저장되어 있는 것을 확인할 수 있습니다.
- 부모와 자식이 다른 트랜잭션이라 트랜잭션 전파는 발생하지 않더라도 예외는 전파되기 때문에 예외처리를 하지 않으면 부모에서 예외가 발생해 롤백됩니다.
- 예외처리 했을 때 문구를 보시면 REQUIRED 에서 발생한 문구가 없는 것을 확인할 수 있습니다.
- 즉, REQUIRES_NEW 에서는 부모와 자식의 트랜잭션이 분리되어 있음을 확인할 수 있습니다.
- MANDATORY
- testMandatory()
public void testMandatory() { User user1 = new User("robbie1", "1234", "mandatory1"); userRepository.save(user1); testChildService.testMandatoryChild(); User user3 = new User("robbie3", "1234", "mandatory3"); userRepository.save(user3); }
- testMandatoryChild()
@Transactional(propagation = Propagation.MANDATORY) public void testMandatoryChild() { User user2 = new User("robbie2", "1234", "mandatory2"); userRepository.save(user2); }
- 부모 메서드인 testMandatory()에 트랜잭션이 없기 때문에 자식 메서드인 testMandatoryChild()에서 오류가 발생합니다.
- No existing transaction found for transaction marked with propagation 'mandatory’
- 위 문구와 함께 오류가 발생하고 부모 메서드에서 실행된 insert 쿼리 하나만 커밋됩니다.
- 부모 메서드에 트랜잭션이 없기 때문에 이미 커밋된겁니다.
- DB를 확인해 보면 nickname: mandatory1 이 저장되어 있는 것을 확인할 수 있습니다.
- NEVER
- testNever()
@Transactional public void testNever() { User user1 = new User("robbie1", "1234", "never1"); userRepository.save(user1); testChildService.testNeverChild(); User user3 = new User("robbie3", "1234", "never3"); userRepository.save(user3); }
- testNeverChild()
@Transactional(propagation = Propagation.NEVER) public void testNeverChild() { User user2 = new User("robbie2", "1234", "never2"); userRepository.save(user2); }
- 부모 메서드인 testNever()에 트랜잭션이 있기 때문에 자식 메서드인 testNeverChild()에서 오류가 발생합니다.
- Existing transaction found for transaction marked with propagation 'never’
- 위 문구와 함께 오류가 발생하고 MANDATORY 와는 다르게 부모 메서드에 오류가 전파되어 롤백됩니다.
- DB를 확인해보면 User는 저장 되어있지 않습니다.
- NESTED
- 부모 트랜잭션에서 오류발생
- testNested()
@Transactional public void testNested() { User user1 = new User("robbie1", "1234", "nested1"); userRepository.save(user1); testChildService.testNestedChild(); User user3 = new User("robbie3", "1234", "nested3"); userRepository.save(user3); throw new RuntimeException(); }
- testNestedChild()
@Transactional(propagation = Propagation.NESTED) public void testNestedChild() { User user2 = new User("robbie2", "1234", "nested2"); userRepository.save(user2); }
- 부모 트랜잭션에서 오류가 발생하면 자식 트랜잭션도 rollback합니다.
- DB를 확인해보면 User는 저장 되어있지 않습니다.
- 자식 트랜잭션에서 오류발생 예외처리
- testNested()
@Transactional public void testNested() { try { User user1 = new User("robbie1", "1234", "nested1"); userRepository.save(user1); testChildService.testNestedChild(); User user3 = new User("robbie3", "1234", "nested3"); userRepository.save(user3); } catch (RuntimeException ex) { ex.printStackTrace(); } }
- testNestedChild()
@Transactional(propagation = Propagation.NESTED) public void testNestedChild() { User user2 = new User("robbie2", "1234", "nested2"); userRepository.save(user2); throw new RuntimeException(); }
- 자식 트랜잭션에서 예외가 발생하더라도 부모 트랜잭션은 rollback하지 않습니다.
- 따라서 예외처리를 하게되면 부모 메서드에서 발생한 insert 쿼리는 커밋됩니다.
- DB를 확인해 보면 nickname: nested1 이 저장되어 있는 것을 확인할 수 있습니다.
- 부모 트랜잭션에서 오류발생
- REQUIRED