development/spring

@Transactional 전파속성 주의사항

bokshiri 2024. 9. 10. 18:56

스프링에서 제공하는 트랜잭션 추상화를 위한 선언적 트랜잭션 @Transactional은 다음과 같은 전파 속성을 갖는다.

REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED)

SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS)

MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY)

REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW)

NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED)

NEVER(TransactionDefinition.PROPAGATION_NEVER)

NESTED(TransactionDefinition.PROPAGATION_NESTED)

1. REQUIRED (Support a current transaction, create a new one if none exists)

선언적 트랜잭션의 Default 전파속성

이미 실행중인 트랜잭션이 있을 경우 참여하고, 없을 경우 새로운 트랜잭션을 시작한다.

 

2. SUPPORTS (Support a current transaction, execute non-transactionally if none exists)

이미 실행중인 트랜잭션이 있을 경우 참여하고, 없을 경우 트랜잭션이 없는 상태로 처리한다.

 

3. MANDATORY (Support a current transaction, throw an exception if none exists)

이미 실행중인 트랜잭션이 있을 경우 참여하고, 없을 경우 Exception을 발생시킴

 

4. REQUIRES_NEW (Create a new transaction, and suspend the current transaction if one exists)

이미 실행중인 트랜잭션이 있을 경우 중단(연기)하고, 새로운 트랜잭션을 생성하여 진행한다.

 

5. NOT_SUPPORTED (Execute non-transactionally, suspend the current transaction if one exists)

이미 실행중인 트랜잭션이 있을 경우 중단하고, 트랜잭션 없이 진행한다.

 

6. NEVER (Execute non-transactionally, throw an exception if a transaction exists)

트랜잭션 없이 진행하며, 이미 실행중인 트랜잭션이 존재할 경우 Exception을 발생시킨다.

 

7. NESTED (Execute within a nested transaction if a current transaction exists, behave like REQUIRED otherwise)

이미 실행중인 트랜잭션이 있을 경우 내부에 중첩(자식) 트랜잭션을 만들어 진행한다. 중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않는다.

 

https://mangkyu.tistory.com/169

 

[Spring] Spring 트랜잭션의 세부 설정(전파 속성, 격리수준, 읽기전용, 롤백/커밋 예외 등) - (2/3)

아래의 내용은 토비의 스프링의 2권 2장을 참고해서 작성하였습니다. 1. Spring 트랜잭션의 세부 설정(전파 속성, 격리수준, 읽기전용, 롤백/커밋 예외 등) [ 전파 속성(Propagation) ] Spring이 제공하는

mangkyu.tistory.com

https://oingdaddy.tistory.com/28#google_vignette

 

Spring Transaction Propagation을 예제를 통해 알아보자

spring에서 transaction propagation 은 전파옵션을 뜻한다. 전파옵션이라는 것은 트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법에 대해 결정하는 속성값 정도로 생각하면 된다. 즉 큰 트랜잭션

oingdaddy.tistory.com

 


보통 실무에서는 DEFAULT 전파속성인 REQUIRED나, 새로운 트랜잭션을 생성하여 진행하는 REQUIRES_NEW를 많이 사용한다. REQUIRED 속성은 트랜잭션을 필요로 하는 전파속성으로서 트랜잭션이 없을 경우 새로 생성하여 진행하고, 이미 실행중인 트랜잭션이 있을 경우 해당 트랜잭션에 참여하여 로직이 수행된다. 여기서, 참여의 의미를 깊게 이해할 필요가 있다.

예시를 보기 전에 우선 선언적 트랜잭션의 동작 원리를 간단하게 살펴보도록 하자.

Spring의 @Transactional은 CGLib(Proxy) 방식의 AOP 기반으로 동작한다. AOP의 @Around Advice를 사용하여 타겟 서비스 앞 뒤로 트랜잭션과 관련된 부가 기능을 주입하는 것이다. 개발자가 @Transactional 어노테이션을 붙여 서비스를 개발한 후 호출하면, 스프링에서는 해당 서비스의 Bean을 감싸는 Proxy Bean을 생성하고 실제 서비스 Bean이 호출되었을 때 Proxy Bean이 요청을 가로채 타겟 메서드 앞 뒤로 트랜잭션 begin과 commit/rollback 을 처리하게 된다.

개발자는 트랜잭션과 관련된 로직을 서비스 코드에 추가할 필요가 없으며, 부가 기능으로 분류되어 스프링에서 자동으로 처리해준다.

Proxy Bean이 타겟 Bean에 대한 요청을 Intercept한 후 내부적으로 처리하는 동작 과정을 소스로 간단하게 나타내면 다음과 같을 것이다.

public void proxyMethod() {
    try {
      // 트랜잭션 시작
      transactionManager.begin();

      // 타겟 메서드 호출
      target.method();

      // 트랜잭션 커밋
      transactionManager.commit();
      
    } catch (Exception e) {
      // 트랜잭션 오류 발생 시 롤백 (언체크 예외인 경우에만 롤백)
      transactionManager.rollback();
      
    }
  }

 

https://velog.io/@ann0905/AOP%EC%99%80-Transactional%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC

 

[Spring] AOP와 @Transactional의 동작 원리

오늘은 @Transactional의 동작 원리를 AOP와 함께 좀 더 자세하게 조사해보려고 한다.여기서 다루는 내용은 다음과 같다.AOP란 무엇이며 왜 사용하는가Spring AOP는 왜 프록시를 사용하는가@Transactional은

velog.io

https://velog.io/@gwichanlee/AOP-Advice

 

AOP - Advice

AOP - Advice ◎ Advice ◎ Advice 종류

velog.io

https://moonong.tistory.com/98

 

[Spring] Spring AOP 이해하기 (2) - @Transactional 의 동작 원리

@Transactional의 동작 원리 @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; @Transactional public void save(User user){ userRepository.save(user); } } @Transactional 을 붙이면 일어나는

moonong.tistory.com

 

원리에 대해 정리했으니 다시 본론으로 돌아가서 트랜잭션 전파 속성의 참여 개념에 대해 살펴보도록 하자. 언뜻 보기에는 굉장히 단순한 단어이다. 서비스 A에서 트랜잭션을 생성하여 수행중이고, 그 내부 로직인 서비스B가 기존 트랜잭션에 참여하도록 되어 있다면 서비스 A와 B는 하나의 트랜잭션으로 묶일 것이다.

여기서 만일 서비스 B에 @Transactional을 선언하고 언체크 예외를 발생시킨 후 서비스A에서 예외를 잡는다면 어떻게 동작할까?

// Service A

...

private final ServiceB service;

@Transactional
public void parent() {
    int a = 1;
    int b = 0;
    try {
    	service.child(a, b);
    } catch (Exception e) {
    	log.error("service A error...", e);
        //throw e;
    }
}

 

// Service B

...

@Transactional(propagation = Propagation.REQUIRED)
public void child(int a, int b) {
   int c = a/b;
}

 

선언적 트랜잭션의 프록시 객체를 염두에 두고 코드의 실행 과정을 정리해보면 다음과 같을 것이다.

1. @Transactional이 선언되어 있는 ServiceA의 Proxy 객체가 생성되고 ServiceA의 parent() 메서드가 호출되었을 때 요청을 가로채 트랜잭션을 시작한다. 이후 실제 ServiceA Bean의 parent() 메서드를 호출한다.

2. parent() 메서드에서 ServiceB의 child() 메서드를 호출한다. 여기서, ServiceB의 child() 메서드 역시 @Transactional이 선언되어 있기 때문에 ServiceB의 Proxy Bean이 요청을 가로챈다. 트랜잭션의 전파속성이 REQUIRED (참여) 이므로 앞서 시작된 ServiceA의 트랜잭션에 참여한 후 ServiceB Bean의 child() 메서드를 호출한다.

3. child() 메서드에 전달된 인자는 a = 1, b = 0 이므로 int c = a/b; 는 RuntimeException(ArithmeticException)을 발생시킨다. 이 예외는 ServiceB의 Proxy Bean으로 전파되고, 언체크 예외이므로 트랜잭션의 속성은 rollback-only로 변경된다.

4. 예외는 ServiceA의 parent() 메서드로 다시 전파된다. parent() 메서드 내부에서 예외처리를 통해 에러 로그만 남기고 예외를 던지지 않으므로, 메서드는 정상 종료된다.

5. ServiceA의 Proxy Bean은 메서드가 정상 종료되었으므로 트랜잭션에 대해 commit 명령을 내린다.

6. 하지만 앞서 해당 트랜잭션은 ServiceB의 child() 메서드에서 발생한 언체크 예외로 인해 rollback-only 상태가 되었기 때문에, commit을 처리하지 못하고 Exception이 발생한다. (트랜잭션 롤백)

 

위 소스의 의도는 ServiceB에서 무언가의 이유로 예외가 발생했을 때, 호출한 곳에서 해당 예외를 잡아 ServiceB의 로직이 실패하더라도 롤백을 막고 나머지 작업을 커밋하는 것이다. 하지만 의도한 것과는 달리 트랜잭션은 롤백되었다. 그 이유는 스프링의 선언적 트랜잭션이 AOP Proxy 방식으로 동작하기 때문이다. 동작원리를 이해하지 못한 채 선언적 트랜잭션을 사용하면 예상치 못한 문제가 발생할 수 있는 것이다.

 

최초에 의도한 대로 코드를 동작시키기 위해서는 ServiceB의 메서드의 선언적 트랜잭션을 제거하면 된다. 

// Service B

...

//@Transactional(propagation = Propagation.REQUIRED)
public void child(int a, int b) {
   int c = a/b;
}

선언적 트랜잭션이 제거되더라도, ServiceA의 메서드가 호출될 때 ServiceA의 Proxy Bean에 의해 트랜잭션이 진행되기 때문에 ServiceB의 child() 메서드는 같은 트랜잭션에 참여하게 될 것이다.

또한 ServiceB의 메서드에서 실패가 발생하더라도 ServiceB의 Proxy Bean이 존재하지 않기 때문에 예외는 곧바로 ServiceA의 메서드로 전파될 것이고, ServiceA의 메서드에서 예외처리를 통해 메서드를 정상 종료시킨다면 ServiceA의 Proxy Bean은 commit 명령을 수행하여 트랜잭션 내용을 정상적으로 반영할 수 있게 된다. 이렇게 되면, ServiceB의 작업 내용이 누락되더라도 나머지 작업분은 데이터베이스에 반영할 수 있는 것이다.