[JPA] @Transactional이 중첩되면 무슨 일이 일어날까?

정민규·2023년 10월 10일
3

스프링 JPA를 사용하던 도중 한가지 궁금한 점이 생겼다.

@Transactional 어노테이션이 붙어있는 메서드에서 @Transactional이 들어간 다른 메서드를 호출하면 어떻게 될까?

예를 들면, 아래와 같은 상황이다.

@Transactional
public Todo addNewTodo(String description){
	Todo newTodo = new Todo(description);
    return saveTodo(newTodo)
}

@Transactional
public Todo saveTodo(Todo todo){
	todoRepository.save(todo);
    return todo;
}

addNewTodo라는 메서드에서 saveTodo라는 메서드를 호출하고 있는데, 두 메서드 모두 @Transactional 어노테이션이 붙어 있는 상태이다.


트랜잭션의 전파 설정

스프링에서는 위 상황과 같이 트랜잭션이 중첩되어 실행되는 경우 어떻게 처리할지에 대한 설정을 제공하고 있다.
이를 트랜잭션 전파 설정이라고 한다.

1. REQUIRED (기본 설정)

  • 별도로 설정을 하지 않으면 REQUIRED 설정이 사용된다. (Default 설정)
  • 부모 트랜잭션이 있는 경우, 새로 트랜잭션을 생성하지 않고 부모 트랜잭션에 합쳐진다.
  • 부모 트랜잭션이 없는 경우에만 자신의 트랜잭션을 새로 생성한다.

2. REQUIRES_NEW

  • 부모 트랜잭션의 존재 여부와 관계없이, 무조건 자신의 트랜잭션을 새로 생성한다.
  • 기존에 실행중이던 부모 트랜잭션은 자식 트랜잭션이 종료될 때까지 대기한다.
  • 자식 트랜잭션이 롤백되더라도 부모 트랜잭션이 롤백되지는 않는다.
  • 자식 트랜잭션이 정상적으로 종료되고 부모 트랜잭션이 롤백되는 경우, 자식 트랜잭션은 롤백되지 않는다.

즉, 부모 트랜잭션과 자식 트랜잭션 사이의 롤백 여부는 서로 독립적이다.

3. NESTED

  • 부모 트랜잭션의 존재 여부와 관계없이, 무조건 자신의 트랜잭션을 새로 생성한다.
  • 기존에 실행중이던 부모 트랜잭션은 자식 트랜잭션이 종료될 때까지 대기한다.
  • 자식 트랜잭션은 부모 트랜잭션과 함께 커밋된다.
  • 자식 트랜잭션이 롤백되는 경우, 부모 트랜잭션도 롤백된다.
  • 자식 트랜잭션이 정상적으로 종료되고 부모 트랜잭션이 롤백되는 경우, 자식 트랜잭션도 같이 롤백된다.

REQURIRES_NEW설정과 비슷하지만, 부모 트랜잭션과 자식 트랜잭션의 롤백 여부가 서로 종속적이라는 점이 다르다.

4. SUPPORTS

  • 부모 트랜잭션이 있는 경우, 새로 트랜잭션을 생성하지 않고 부모 트랜잭션에 합쳐진다.
  • 부모 트랜잭션이 없는 경우, 트랜잭션을 적용하지 않는다.

5. NOT_SUPPORTED

  • 부모 트랜잭션이 있는 경우, 부모 트랜잭션을 일시중지시킨다.
    이후 트랜잭션을 적용하지 않은 채로 메서드를 수행하고, 메서드가 종료된 후 부모 트랜잭션을 재개한다.
  • 부모 트랜잭션이 없는 경우, 트랜잭션을 적용하지 않는다.

6. MANDATORY

  • 부모 트랜잭션이 있는 경우, 새로 트랜잭션을 생성하지 않고 부모 트랜잭션에 합쳐진다.
  • 부모 트랜잭션이 없는 경우, 예외가 발생한다.

트랜잭션이 반드시 필요하지만 자식 트랜잭션(중첩 트랜잭션)을 생성하지는 않는 경우에 사용한다.

7. NEVER

  • 부모 트랜잭션이 있는 경우, 예외가 발생한다.
  • 부모 트랜잭션이 없는 경우, 트랜잭션을 적용하지 않는다.

즉, 트랜잭션을 절대 적용해서는 안되는 경우에 사용한다.


사용법과 주의할 점

아래 코드는 위쪽 예시에 나온 메서드를 하나의 클래스로 묶어 놓은 것이다.

@Service
public class TodoAdder{
    @Transactional
    public Todo addNewTodo(String description){
        Todo newTodo = new Todo(description);
        return saveTodo(newTodo)
    }

    @Transactional(propagation = Propagation.NESTED)
    public Todo saveTodo(Todo todo){
        todoRepository.save(todo);
        return todo;
    }
}

트랜잭션 전파 설정은 @Transactional어노테이션에 propagation 속성을 지정해 줌으로써 사용할 수 있다.

위 코드는 saveTodo 메서드에 NESTED 전파 설정을 사용하여 자식 트랜잭션을 생성하게 만든 모습이다.


다만, 위 코드를 실제로 실행해 보면 우리가 의도했던 바와는 달리 자식 트랜잭션이 생성되지 않고 addNewTodo의 트랜잭션만 생성된다.

그 이유는 두 메서드가 하나의 클래스로 묶여있기 때문이다.

스프링의 트랜잭션 기능은 스프링 AOP의 Proxy를 기반으로 작동한다. (스프링 Proxy에 대해서는 나중에 더 자세히 다뤄 보아야겠다.)

@Transactional 어노테이션이 붙은 메서드가 실행되는 경우, Spring AOP에서 프록시 객체를 만들어 해당 메서드 실행 전/후로 트랜잭션 시작/커밋을 하는 코드를 추가해 주는 방식으로 동작한다.

이때 Spring에서 @Transactional 어노테이션이 붙은 메서드의 호출을 가로챌 수 있어야 하는데, Spring AOP에서는 기본적으로 같은 객체 내부에서의 메서드 호출을 가로챌 수가 없다고 한다.

그래서 addNewTodo 메서드의 트랜잭션을 생성하고 saveTodo를 호출했을 때 트랜잭션이 따로 생성되지 않는 것이다.

위 문제를 해결하는 가장 간단한 방법은 두 메서드를 서로 다른 두 클래스로 분리하는 것이다.

@Service
public class TodoAdder{
    @Transactional
    public Todo addNewTodo(String description){
        Todo newTodo = new Todo(description);
        return saveTodo(newTodo)
    }
}
---------------------------------------------
@Service
public class TodoAdder2{
    @Transactional(propagation = Propagation.NESTED)
    public Todo saveTodo(Todo todo){
        todoRepository.save(todo);
        return todo;
    }
}

참고자료

https://stackoverflow.com/questions/28480480/propagation-requires-new-does-not-create-a-new-transaction-in-spring-with-jpa

https://stackoverflow.com/questions/53447687/transactional-propagation-requires-new-not-work

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Propagation.html

profile
조금이더라도 꾸준하게.

0개의 댓글