프로그래밍/Java

[Spring, Spring Boot] @Transactional의 중첩에 대하여

차나니 2024. 4. 12. 19:00

트랜잭션의 개념은 여기를 클릭하여 확인해보세요 !

 

@Transactional이  중첩이 되었을 때

업무 중 @Transactional이 중첩이 되었을 때(아래와 같은 상황) 어떻게 진행될지 궁금해졌다 !

@Transactional
public Info buy(String item){
	payment newPayment = new payment(item);
    return payment(newPayment)
}

@Transactional
public Info payment(Info info){
	buyRepository.save(info);
    return info;
}

buy 메서드에서 payment 메서드를 호출하고 있는데, 두 메서드 모두 @Transactional 어노테이션이 적용되어 있는 상태입니다.

 

트랙잭션의 전파 설정

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

이를 트랜잭션 전파 설정이라고 합니다 !

 

REQUIRED (기본 설정)

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

REQUIRES_NEW

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

NESTED

  • 부모 트랜잭션의 존재 여부와 관계없이, 무조건 자신의 트랜잭션을 생성합니다.
  • 기존에 실행중이던 부모 트랜잭션은 자식 트랜잭션이 종료될 때까지 대기합니다.
  • 자식 트랜잭션은 부모 트랜잭션과 함께 커밋됩니다.
  • 자식 트랜잭션이 롤백되는 경우, 부모 트랜잭션도 롤백됩니다.
  • 자식 트랜잭션이 정상적으로 종료되고 부모 트랜잭션이 롤백되는 경우, 자식 트랜잭션도 같이 롤백됩니다.
  • REQUIRES_NEW 설정과 비슷하지만, 부모와 자식 트랜잭션의 롤백 여부가 서로 종속적이라는 점이 다릅니다.

SUPPORTS

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

NOT_SUPPORTED

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

MANDATORY

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

NEVER

  • 부모 트랜잭션이 있는 경우, 예외가 발생합니다.
  • 부모 트랜잭션이 없는 경우, 트랜잭션을 적용하지 않습니다.
  • 즉, 트랜잭션을 절때 적용해서는 안되는 경우에 사용합니다.

 

사용법과 주의할 점

아래 코드는 위의 예시 코드의 메서드를 클래스로 묶어 놓은 것입니다.

@Service
public class Shop {
	
    @Transactional
    public Info buy(String item){
        payment newPayment = new payment(item);
        return payment(newPayment)
    }

    @Transactional(propagation = Propagation.NESTED)
    public Info payment(Info info){
        buyRepository.save(info);
        return info;
    }
	
}

 

트랜잭션 전파 설정은 @Transactional 어노테이션에 propagation 속성을 지정하여 사용할 수 있습니다.

위 코드는 payment 메서드에 NESTED 전파 설정을 사용하여 자식 트랜잭션을 생성하게 만드는 형태입니다.

 

하지만, 위 코드는 실제로 실행해 보면 의도한 것과는 다르게 자식 트랜잭션이 생성되지 않고 buy 메서드의 트랜잭션만 생성됩니다.

 

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

스프링의 트랜잭션 기능은 스프링 AOP의 Proxy를 기반으로 작동합니다. 

 

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

 

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

 

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

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

@Service
public class Shop {
	
    @Transactional
    public Info buy(String item){
        payment newPayment = new payment(item);
        return payment(newPayment)
    }

}

----------------------------------------------
@Service
public class Shop2 {
	
    @Transactional(propagation = Propagation.NESTED)
    public Info payment(Info info){
        buyRepository.save(info);
        return info;
    }
	
}