Spring/Framwork

트랜잭션 AOP 주의 사항 (+ proxy 내부 호출, 초기화 시점)

밍구밍구밍 2024. 9. 11. 17:41

◎ proxy 내부 호출

 

먼저 @Transactional 을 적용하면 proxy 객체가 먼저 요청을 받아서 트랜잭션을 처리하고, 실제 객체를 호출해준다.

만약 proxy 를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP 가 적용되지 않고, 트랜잭션도 적용되지 않는다.

 

공부한 내용을 자세히 정리하였다..

 

 

코드를 통해 알아보자

먼저 전체적인 코드 로직은 아래와 같다.

@Slf4j
@SpringBootTest
public class InternalCallV1Test {

    @Autowired
    CallService callService;

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

    @Test
    void internalCall() {
        callService.internal();
    }

    @Test
    void externalCall() {
        callService.external();
    }

    @TestConfiguration
    static class internalCallV1TestConfig {

        @Bean
        CallService callService() {
            return new CallService();
        }
    }

    @Slf4j
    static class CallService {

        public void external() { // 외부에서 호출하는 메서드 (spring Bean 으로 등록이 안되있기 떄문에 프록시 생성이 안된다.) 트랜잭션은 수행 된다.
            log.info("call external");
            printTxInfo();
            internal();
        }

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

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

 

private void printTxInfo() {
    boolean txActive = TransactionSynchronizationManager.isActualTransactionActive(); // 트랜잭션이 적용 되었는지 여부를 확인하는 메서드
    log.info("active={}", txActive);
    boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); // 트랜잭션이 readOnly 인지 확인하는 메서드
    log.info("tx readOnly={}", readOnly);
}

printTxInfo() 설정 정보 확인 : 트랜잭션의 적용 여부와 readOnly 의 타입을 확인하는 메서드

 

1. internal()

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

internal 메서드의 log 결과에서 active = true 로 트랜잭션이 적용되는 것을 볼 수 있다.

※ 그림 참조 : internal() 내부 트랜잭션 적용 로직

현재 internal() 메서드는 트랜잭션이 적용 되어 있는 메서드이다.

 

 

2. external() 

public void external() { // 외부에서 호출하는 메서드 (spring Bean 으로 등록이 안되있기 떄문에 프록시 생성이 안된다.) 트랜잭션은 수행 된다.
    log.info("call external");
    printTxInfo();
    internal();
}

external 메서드에서 트랜잭션이 당연히 적용 되지 않았다.

※ 그림 참조 : external() 내부 로직

external() 메서드는 트랜잭션이 적용 되어 있지 않은 메서드이다.

 

당연히 internal() 메서드는 트랜잭션이 적용 되어 있기 때문에 Spring Bean 에 등록되고 프록시 객체가 생성된다.

반대로 external() 메서드는 트랜잭션이 적용 되어 있지 않고 Spring Bean 에 등록되지 않았기 때문에 일반적인 메서드로 취급된다.

 

그런데 여기 external() 메서드에서 proxy 가 적용 되었던 internal() 메서드를 다시 한번 호출하고 있는 것을 볼 수 있다. (실무에서는 이런 경우도 발생 할 수 있다)

이렇게 되면 internal() 은 프록시와 트랜잭션이 적용된 메서드일까?

 

위의 external() 메서드를 실행한 후 log 에서 확인 할 수 있듯이 internal 은 active = false 를 출력하는 것을 볼 수 있다.

즉, Transactional 이 적용되지 않은 일반 메서드에서 proxy, transcational 메서드를 호출 하게 되면 해당 proxy 메서드는 프록시와 Transactional 기능을 모두 잃어버린 일반 메서드로 취급 된다는 것이다.

 

※ 이유 :  

자바 언어에서 메서드 앞에 별도의 참조가 없으면 this 인 자신의 인스턴스를 가리킨다.

결과적으로 위의 public void external(){} 인 @Transactional 이 적용되지 않은 메서드에서 외부 메서드를 호출하게 되면 위의 그림에 있는 target 에 있는 일반 메서드 internal() 을 호출 하게 되는 것이다.

 

이것이 프록시 방식의 AOP 한계이다. 

 

해결 방안

- internal() 메서드와 같은 프록시 객체를 트랜잭션이 아닌 외부에서 호출할 때 프록시 객체가 생성되도록 하는 방법은 아래와 같이 트랜잭션이 적용된 internal() 을 별도의 클래스로 구분하는 방법이 있다

@Slf4j
@SpringBootTest
public class InternalCallV2Test {

    @Autowired
    CallService callService;

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

    @Test
    void externalCallV2() {
        callService.external();
    }

    @TestConfiguration
    static class internalCallV2TestConfig {

        @Bean
        CallService callService() {
            return new CallService(internalService());
        }

        @Bean
        InternalService internalService() {
            return new InternalService();
        }
    }

    @Slf4j
    @RequiredArgsConstructor
    static class CallService {

        private final InternalService internalService;

        public void external() { // 외부에서 호출하는 메서드 (spring Bean 으로 등록이 안되있기 떄문에 프록시 생성이 안된다.) 트랜잭션은 수행 된다.
            log.info("call external");
            printTxInfo();
            internalService.internal(); // 별도로 분리된 internalService 를 통해 internal() 를 호출하면 트랜잭션과 프록시가 적용 된다.
        }

        private void printTxInfo() {
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive(); // 트랜잭션이 적용 되었는지 여부를 확인하는 메서드
            log.info("active={}", txActive);
        }
    }

    // InternalService 를 '별도의 클래스로 분리'해서 해당 프록시를 외부에서 호출할 수 있도록 설정
        static class InternalService {
        
            @Transactional
            public void internal() {
                log.info("call internal");
                printTxInfo();
            }

            private void printTxInfo() {
                boolean txActive = TransactionSynchronizationManager.isActualTransactionActive(); // 트랜잭션이 적용 되었는지 여부를 확인하는 메서드
                log.info("active={}", txActive);
                boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); // 트랜잭션이 readOnly 인지 확인하는 메서드
                log.info("tx readOnly={}", readOnly);
            }
        }
    }

 

 

internalService 클래스를 외부 클래스인 CallService 에 의존관계 주입을 통해 외부에서도 InternalService 의 클래스를 참조하여 internal() 메서드를 호출할 수 있도록 개선하면 external() 메서드에서도 트랜잭션이 적용된 internal() 메서드를 호출 할 수 있게 된다.

external() 메서드를 호출한 log 확인 external = false, internal = true 인것을 확인 할 수 있다.

◎ 초기화 시점

초기화 코드에 @PostConstruct 를 선언하면 @Transcationl 이 걸려 있어도 트랜잭션 적용이 되지 않는다

 

code ex)

@Slf4j
static class Hello {

    @PostConstruct // 종속성 주입이 완료된 후 초기화를 수행하기 위해 실행해야 하는 메서드에 사용 (?)
    @Transactional
    public void initV1() {
        boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
        log.info("Hello init @PostConstruct tx active={}", isActive);
    }

    public void initV2() {

    }
}
@PostConstruct // 종속성 주입이 완료된 후 초기화를 수행하기 위해 실행해야 하는 메서드에 사용 (?)
@Transactional
public void initV1() {
    boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("Hello init @PostConstruct tx active={}", isActive);
}

initV1() 메서드에 @PostConstruct 와 @Transactional 어노테이션을 같이 사용하여 외부에서 아래와 같이 go() 라는 메서드를 통해 initV1() 메서드를 호출 하였다.

 

@SpringBootTest
public class InitTxTest {

    @Autowired
    Hello hello;

    @Test
    void go() {
        // 초기화 코드는 스프링이 초기화 시점에 호출된다 (@PostConstruct 기능) 즉, initV1() 을 호출해준다. (신기..)
    }

    @TestConfiguration
    static class InitTxTestConfig {

        @Bean
        Hello hello() {
            return new Hello();
        }
    }

active = false (initV1 에 트랜잭션이 적용되지 않았다)

※ 이유 : 초기화 코드가 먼저 호출 되고, 그 다음에 트랜잭션 AOP 가 적용되기 때문이다. 따라서 초기화 시점에는 해당 메서드 트랜잭션 획득 불가

 

이러한 경우를 개선한 코드는 아래와 같다 - initV2()

@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2() {
    boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("Hello init @ApplicationReadyEvent tx active={}", isActive);
}

- 앞서, @PostConstruct 와 달리 스프링에서 제공하는 EventListner(ApplicationReadyEvent.class) 를 적용하면 스프링은 컨테이너가 완전히 생성되고 난 다음 이벤트가 붙은 메서드를 호출해 준다. 따라서 initV2() 는 트랜잭션이 적용된다.

 

※ 정리 : 트랜잭션 내부에서 수행을 해야될때는 @EventListener 를 사용하고, 일반적인 초기화를 진행할 때 (트랜잭션 미적용)에는 @PostConstruct 를 사용하면 된다.