@Transactional사용 시 주의 사항과 원인

2024. 9. 21. 21:58Backend 취업준비/Spring

1. 프록시와 내부 호출의 문제

 

@Transactional이 적용된 메서드(코드 예시의 internal())를 @Transactional이 적용되지 않은 메서드(코드 예시의 external()) 안에서 사용하게 되면, external()메서드를 사용할 때 우리의 기대와 다르게 internal() 에서 트랜잭션이 전혀 적용되지 않는다. 이는 @Transactional 이 작동하는 프록시 객체의 원리에 있다.


코드 예시

@Slf4j
static class CallService {

    public void external() {
        log.info("callExternal");
        printTxInfo();
        internal();
    }

    @Transactional
    public void internal() {
        log.info("callInternal");
        printTxInfo();
    }

    private void printTxInfo() {
        boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
        log.info("tx active={}", txActive);
    }

}

  1. callService.external() 가 실행된다.
  2. callService 의 트랜잭션 프록시가 호출된다. external() 메서드에는 @Transactional 이 없다. 프록시 인스턴스는 실제 인스턴스에 할 일을 단순 위임한다.
  3. 따라서 트랜잭션 적용되지 않고, 실제 callService 객체 인스턴스의 external() 을 호출한다.
  4. external() 은 내부에서 internal() 메서드를 호출한다.
  5. 메서드 앞에 별도의 참조가 없으면 this 가 생력되어 있고, 이는 자기 자신의 인스턴스를 가리킨다.
  6. 결과적으로 자기 자신의 내부 메서드를 호출하는 this.internal() 이 되는데, 여기서 this 는 자기 자신을 가리키
    므로, 실제 대상 객체( target )의 인스턴스를 뜻한다.
  7. 실제 인스턴스에는 트랜잭션을 위한 로직이 없다. 프록시 인스턴스를 통해서 호출 되어야 트랜잭션이 적용됨

이를 실용적으로 해결하려면 내부 호출 방식을 채택하지 않고 클래스를 나누어 외부에서 호출한다.

 

 

2. private 메서드에는 트랜잭션 적용 X

 

  • 스프링의 트랜잭션 AOP 기능은 private 메서드에는 트랜잭션이 적용되지 않도록 설정되어 있다.
  • public, protected , default 는 외부에서 호출이 가능하다. 트랜잭션은 주로 비즈니스 로직의 시작점에 걸기 때문에 대부분 외부에 열어준 곳을 시작점으로 사용한다.
  • 따라서 private 메서드에서는 트랜잭션을 적용이 안된다. 클래스 레벨이 아니라 따로 메서드에 @Transactional을 달아 주어도 적용되지 않는다.

 

과거 스프링 부트 버전(스프링 부트 3.0 이전)에서는 protected , default 의 경우에서도 @Transactional이 적용이 안되었으며, 해당 접근제어자를 가진 메서드 레벨에 @Transactional이 붙어있어도 컴파일 오류가 나지 않아서 많은 주의가 요구 된다. 스프링 부트 3.0 이후 부터는 private 메서드 레벨에 @Transactional 이 붙어있으면 컴파일 오류가 난다. (클래스 레벨에 붙어있을 때는 오류 안남)

 

3. @PostConstruct와 함께 사용시 의도대로 작동하지 않는다

스프링 빈의 이벤트 라이프사이클은 다음과 같다.

  • 스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백(@PostConstruct) -> 사용 -> 소멸전 콜백 -> 스프링 종료

빈을 초기화 하면서 트랜잭션을 적용하고 싶다고 다음과 같이 설정하면 트랜잭션이 적용되지 않는다. 이유는 다음과 같다

  • @PostConstruct는 해당 빈 자체만 생성되었다고 가정하고 호출된다. 
  • 이는 해당 빈에 관련된 AOP등을 포함한, 전체 스프링 애플리케이션 컨텍스트가 초기화 된 것을 의미하지는 않는다.
  • 트랜잭션을 처리하는 AOP등은 스프링의 후 처리기(post processer)가 완전히 동작을 끝내서, 스프링 애플리케이션 컨텍스트의 초기화가 완료되어야 적용된다.
@SpringBootTest
@Slf4j
public class IntiTxTest {

    @Autowired
    Hello hello;

    @Test
    void go() {
        log.info("hello class ={}", hello.getClass());
    }

    @TestConfiguration
    static class InitTxTestConfig {
        @Bean
        Hello hello() {
            return new Hello();
        }
    }

    @Slf4j
    static class Hello {

        @PostConstruct
        @Transactional
        public void initV1() {
            boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("Hello init @PostConstruct tx active={}", isActive);
        }

    }

}


스프링 빈의 라이프사이클과 관련된 내용을 더 자세히 정리하면 다음과 같다

  • 스프링 컨테이너 생성
  • 스프링 빈 생성
  • 의존관계 주입
  • 빈 처리기 (BeanPostProcessor) - 전처리, postProcessBeforeInitialization()
  • 초기화 콜백 (@PostConstruct)
    빈이 완전히 생성되고 의존성 주입이 완료된 후, 초기화 메서드가 호출됩니다. @PostConstruct 애노테이션이 붙은 메서드 호출됨.
  • 빈 처리기 (BeanPostProcessor) - 후처리, postProcessAfterInitialization()
    초기화가 끝난 후 다시 BeanPostProcessor가 동작하여 프록시 객체 생성 등의 후처리가 진행. 이 과정에서 AOP 프록시가 생성.
  • 빈 등록 완료
    모든 후처리 작업이 끝난 후, 빈이 스프링 컨테이너에 등록.
  • 스프링 애플리케이션 컨텍스트 초기화 완료 및 어플리케이션 실행
  • 사용
  • 소멸 전 콜백 (@PreDestroy, DisposableBean의 destroy)
    애플리케이션이 종료되기 전에 빈의 소멸 콜백 메서드가 호출. @PreDestroy 애노테이션이 붙은 메서드 호출
  • 스프링 종료
    스프링 컨테이너가 종료되고, 모든 빈이 소멸

 

 

번 외로 해당 테스트를 실행할 때 로그에는 hello에 프록시 객체로 찍히게 되는데 그 이유는

 

해당 메서드가 실행되는 시점이 사용 시점에 해당하기 때문이다

 

 

 

BeanPostProcessor의 두 메서드

 

 

 

AOP 프록시 객체의 생성 방식에 조금 더 자세히 하자면

  • @TransactionalAOP 프록시 객체 과 같은 AOP 기능을 사용하면 프록시 객체가 생성. 이 프록시는 일반적으로 CGLIB 방식으로 생성.
  • 프록시 객체를 만드는 주체는 AnnotationAwareAspectJAutoProxyCreator인데, 이는 AOP 적용 대상이 되는 빈을 프록시로 감싸기 위해 사용되는 클래스.
  • AnnotationAwareAspectJAutoProxyCreator는 BeanPostProcessor 인터페이스를 구현한 클래스입니다. 따라서 스프링의 AOP 기능은 BeanPostProcessor를 활용하여 프록시 객체를 생성하고 관리

 

 

스프링 고급편을 안듣고 DB2로 왔더니 좀 헤맸다. 얼른 공부해야할듯

 

 


출처

김영한의 스프링 DB 2편 - 데이터 접근 활용 기술