프록시 내부 호출 문제

학습 페이지

실무에서 자주 만나기도 하고, 많은 개발자들이 고통받는 케이스라고 한다… 분명히 트랜잭션을 적용했는데 적용이 안되고, 롤백했는데 롤백이 안되는 케이스가 바로 이 케이스이다…

반드시 한번은 당하게 되는 케이스라고 하니 집중하자!!

스프링 트랜잭션을 이용하면, 항상 프록시를 통해서 대상 객체를 호출하게끔 한다. 이렇게 해야 프록시에서 먼저 트랜잭션을 열고 이후에 핵심 로직들을 호출할 수 있기 때문임.

그런데…? 프록시를 거치지 않고 대상객체를 호출하게 되면 어떻게 될까? 당연히 트랜잭션이 적용되지 않을 것이다!

물론 앞서 말했듯이, AOP를 이용하면 스프링이 컨테이너에 애초에 프록시 객체를 등록해버리기 때문에 대상 객체를 직접 호출하는 일은 거의 일어나지 않는다. 그렇다면 이런 드문일이 언제 일어날까?

바로 대상 객체 내부에서 스스로 트랜잭션 메서드 호출이 발생할때이다. 이때는 프록시를 거치지 않고 호출되는 문제가 생긴다. 이러면 트랜잭션이 적용되지 않는다.

내부호출 예제

위의 내용을 바로 코드로 보면 이해가 될 것이다.

package hello.springtx.apply;

import lombok.extern.slf4j.Slf4j;
import org.assertj.core.error.DescriptionFormatter;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.core.AutoConfigureCache;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Description;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@SpringBootTest
@Slf4j
public class InternalCallV1Test {

    @Autowired CallService callService;

    @TestConfiguration
    static class InternalCallV1TestConfig {
        @Bean
        CallService callService() {
            return new CallService();
        }
    }

    @Test()
    @DisplayName("프록시 여부 확인 테스트")
    void printProxy() {
        log.info("aop class={}", callService.getClass());
    }

    @Test
    @DisplayName("트랜잭션이 붙은 내부 메서드 자체 실행, 트랜잭션 적용됨")
    void internalCall() {
        callService.internal();
    }

    @Test
    @DisplayName("트랜잭션이 붙은 외부 메서드를 외부 메서드에서 호출시 트랜잭션 적용 안됨")
    void externalCall() {
        callService.external();
    }

    static class CallService {
        public void external() {
            log.info("call external");
            printTxInfo();
            internal(); //문제의 부분...
        }

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

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

재미있는건 인텔리제이도 이런 구성을 바로 경고해준다.

image.png

external() 을 호출하는 테스트의 결과를 보자….