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를 주입받아 사용할 수 있습니다.
    @Transactional 테스트
    • 트랜잭션이 적용되어 DB 작업이 성공했습니다.
    @Test
    @DisplayName("메모 생성 실패")
    void test2() {
        Memo memo = new Memo();
        memo.setUsername("Robbie");
        memo.setContents("@Transactional 테스트 중!");
    
        assertThrows(TransactionRequiredException.class, () -> { // 발생되는 오류를 테스트 할 수 있습니다.
            em.persist(memo); // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
        });
    }
    
    • 트랜잭션이 적용되지 못해 작업이 취소되었습니다.
@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
      • 트랜잭션 격리 수준을 설정하는 데 사용됩니다.
      • 격리 수준의 종류
        1. DEFAULT : 데이터베이스 기본 격리 수준을 사용합니다.
          1. 대부분의 데이터베이스 에서는 READ_COMMITTED 격리 수준을 기본으로 사용합니다.
        2. READ_UNCOMMITTED : 커밋되지 않은 데이터를 읽을 수 있습니다.
          1. 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽을 수 있기 때문에, 이 격리 수준에서는 DirtyRead 문제가 발생할 수 있습니다.
        3. READ_COMMITTED : 커밋된 데이터만 읽을 수 있습니다.
          1. 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽지 않으므로 Dirty Read 문제는 발생하지 않지만, Non-Repeatable Read 문제가 발생할 수 있습니다.
        4. REPEATABLE_READ : 같은 쿼리를 실행해도 결과가 항상 동일합니다.
          1. Non-Repeatable Read 문제는 발생하지 않지만, Phantom Read 문제가 발생할 수 있습니다.
        5. SERIALIZABLE : 모든 트랜잭션을 순차적으로 실행합니다.
          1. Dirty Read, Non-Repeatable Read, Phantom Read 문제는 발생하지 않지만, 성능이 매우 저하될 수 있습니다.
    • propagation
      • **@Transactional**애노테이션이 있는 메서드에서 이미 시작된 트랜잭션이 있을 경우 이 트랜잭션을 사용할지, 새로운 트랜잭션을 시작할지를 지정합니다.
      • 종류
        1. REQUIRED : 이미 시작된 트랜잭션이 있으면 해당 트랜잭션을 사용하고, 없으면 새로운 트랜잭션을 시작합니다. 기본값입니다.
          1. 중간에 자식/부모에서 롤백이 발생된다면 자식과 부모 모두 롤백 합니다.
        2. REQUIRES_NEW : 항상 새로운 트랜잭션을 시작합니다. 이미 시작된 트랜잭션은 일시 중단됩니다.
          1. NESTED 방식으로 메서드 호출이 이루어지더라도 롤백은 각각 이루어 집니다.
        3. SUPPORTS : 이미 시작된 트랜잭션이 있으면 해당 트랜잭션을 사용하고, 없으면 트랜잭션 없이 실행합니다.
        4. MANDATORY : 이미 시작된 트랜잭션이 있으면 해당 트랜잭션을 사용하고, 없으면 예외를 던집니다.
        5. NOT_SUPPORTED : 트랜잭션 없이 실행합니다. 이미 시작된 트랜잭션은 일시 중단됩니다.
        6. NEVER : 트랜잭션 없이 실행합니다. 이미 시작된 트랜잭션이 있으면 예외를 던집니다.
        7. NESTED : 부모 트랜잭션이 존재하면 부모 트랜잭션에 중첩시키고, 부모 트랜잭션이 존재하지 않는다면 새로운 트랜잭션을 생성합니다.
          1. 부모 트랜잭션에 예외가 발생하면 자식 트랜잭션도 롤백 합니다.
          2. 자식 트랜잭션에 예외가 발생하더라도 부모 트랜잭션은 롤백하지 않습니다.
            1. 롤백은 부모 트랜잭션에서 자식 트랜잭션을 호출하는 지점까지만 롤백 됩니다. 이후 부모 트랜잭션에서 문제가 없으면 부모 트랜잭션은 끝까지 commit 됩니다.
    • readOnly
      • 트랜잭션에서 데이터를 읽기만 할 때 사용됩니다.
      • 이 속성을 사용하면 읽기 작업에 대한 최적화를 수행할 수 있습니다.
      • 해당 트랜잭션에서 데이터를 수정하려고 하면 예외가 발생합니다.
    • rollbackFor
      • 특정 예외가 발생했을 때 트랜잭션을 롤백하는데 사용됩니다.
      @Transactional(rollbackFor = Exception.class)
      public void Method() {
          // 트랜잭션 처리 코드
      }
      
      • 모든 예외가 발생하면 트랜잭션을 롤백하도록 설정할 수 있습니다.
    • noRollbackFor
      • 특정 예외가 발생했을 때 트랜잭션을 롤백하는데 사용됩니다.
      @Transactional(noRollbackFor = {IOException.class, TimeoutException.class})
      public void Method() {
          // 트랜잭션 처리 코드
      }
      
      • **IOException.class**와 **TimeoutException.class**를 지정하여 이 두 예외가 발생해도 트랜잭션을 롤백하지 않도록 설정할 수 있습니다.

트랜잭션 깊게 이해하기

  • 트랜잭션의 성질(ACID)
    • 트랜잭션은 네 가지의 성질인 ACID를 가지고 있습니다.
    원자성(Atomicity)
    • 트랜잭션은 작업의 일부분이라도 실패할 경우 전체 작업이 취소됩니다.
    • 즉, 모든 쿼리문이 성공적으로 수행 되어야만 데이터베이스에 변경이 반영됩니다.
    • 이렇게 함으로써 데이터의 무결성을 보장할 수 있습니다.
    일관성(Consistency)
    • 트랜잭션이 수행되기 전과 수행된 후에도 데이터베이스가 일관성 있는 상태를 유지해야 합니다.
    • 즉, 트랜잭션 전후에 데이터베이스의 제약 조건이 만족되어야 합니다.
    격리성(Isolation)
    • 트랜잭션은 다른 트랜잭션의 작업에 영향을 받지 않고 독립적으로 수행되어야 합니다.
    • 여러 개의 트랜잭션이 동시에 수행될 때, 각각의 트랜잭션은 서로를 모르고 자신만의 데이터베이스를 수정하는 것처럼 동작해야 합니다.
    지속성(Durability)
    • 트랜잭션이 성공적으로 수행된 후에는 영구적인 데이터베이스의 변경을 보장해야 합니다.
    • 즉, 시스템이 장애가 발생하더라도 영구적으로 저장된 데이터는 손실되지 않아야 합니다.

 

 

  • 트랜잭션의 상태
    • 트랜잭션은 여러 단계를 거쳐 실행됩니다. 이러한 단계를 트랜잭션의 상태라고 합니다.
    활동(Active)
    • 트랜잭션이 실행 중인 상태를 말합니다.
    실패(Failed)
    • 트랜잭션 실행 중 오류가 발생하여 작업이 실패한 상태를 말합니다.
    부분 완료(Partially Committed)
    • 트랜잭션의 모든 쿼리문이 성공적으로 수행되었지만, 아직 커밋되지 않은 상태를 말합니다.
    완료(Committed)
    • 트랜잭션의 모든 쿼리문이 성공적으로 수행되었고, 커밋된 상태를 말합니다.
    중단(Aborted)
    • 트랜잭션이 롤백되어 중단된 상태를 말합니다.

 

 

  • 트랜잭션의 제어
    • 트랜잭션을 제어하는 명령어는 크게 커밋(Commit)과 롤백(Rollback)이 있습니다.
    커밋(Commit)
    • 트랜잭션의 작업을 영구적으로 데이터베이스에 반영하는 명령어입니다.
    롤백(Rollback)
    • 트랜잭션의 작업을 모두 취소하고, 이전 상태로 되돌리는 명령어입니다.
  • 트랜잭션의 격리 수준(Isolation Level)
    • 트랜잭션 격리 수준은 여러 개의 트랜잭션이 동시에 수행될 때, 각각의 트랜잭션이 다른 트랜잭션의 변경 작업을 어느 정도까지 알아볼 수 있는지를 결정합니다.
    • 격리 수준이 높을수록 데이터베이스의 일관성과 무결성을 보장할 수 있지만, 동시성이 떨어지는 단점이 있습니다.
    • 트랜잭션 격리 수준에는 다음과 같은 종류가 있습니다.
    • 격리 수준****(Isolation Level)****
      • 다른 트랜잭션에서 커밋되지 않은 데이터 변경 내용을 읽을 수 있습니다.
        • 따라서 Dirty Read 문제가 발생할 가능성이 있습니다.
      • 가장 낮은 격리 수준이기 때문에 동시성은 가장 높지만 일관성과 무결성은 보장되지 않습니다.
      READ COMMITTED
      • 트랜잭션에서 변경 내용이 커밋 되어야 값을 읽을 수 있습니다.
        • 한 트랜잭션에서 변경한 데이터를 다른 트랜잭션이 읽을 수 없고, 커밋된 데이터만 읽을 수 있습니다.
        • 즉, 다른 트랜잭션에서 수정 중인 데이터를 읽을 수 없습니다.
      • 따라서 Dirty Read 문제는 발생하지 않지만, Non-Repeatable Read 문제가 발생할 수 있습니다.
      REPEATABLE READ
      • 트랜잭션 내에서 같은 쿼리를 실행하면 항상 같은 결과가 반환되도록 보장합니다.
      • 트랜잭션에서 읽은 데이터에 대해 공유잠금(shared lock)을 걸어 다른 트랜잭션에서 변경하지 못하도록 합니다.
      • 일반적으로 동시성을 보장하면서 일관성과 무결성을 보장하기 위해 사용되는 격리 수준입니다.
      • Phantom Read 문제가 발생할 수 있습니다.
      • 대용량 트랜잭션 처리 시 너무 많은 로우 락을 유발하여 성능 저하를 야기할 수 있습니다.
      SERIALIZABLE
      • 최고 수준의 격리 수준입니다.
      • 한 트랜잭션이 완료될 때까지 다른 트랜잭션에서 해당 데이터를 변경할 수 없습니다.
      • 모든 읽기와 쓰기 작업이 테이블 단위 락을 획득하여 직렬화되어 실행됩니다.
        • 이러한 락 때문에 다른 트랜잭션에서는 해당 데이터에 대한 작업을 수행할 수 없으므로 동시성이 제한됩니다.
        • 따라서 동시성이 제일 떨어지는 격리 수준입니다.
      • 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 이 저장되어 있는 것을 확인할 수 있습니다.