프록시 내부 호출(중요!!!)

바그다드·2023년 8월 2일
0

스프링 트랜잭션

목록 보기
2/5

먼저 프록시를 이용한 트랜잭션 처리 과정을 보자.

실제 비즈니스 로직은 프록시 객체가 호출하고 있는 것을 확인할 수 있다. 비즈니스 로직에서 @Transactional을 사용하고 있기 때문에 프록시 객체가 생성된 것인데, 만약 서비스 로직 내부에서 @Transactional이 붙어있지 않은 메서드가 @Transactional 메서드를 호출하면?

  • 트랜잭션이 적용되지 않는다.
    • 트랜잭션은 프록시 객체에서 처리를 하기 때문에 실제 서비스 객체에서 내부 호출로 수행되는 메서드는 @Trancational이 붙어있더라도 트랜잭션이 적용되지 않는다.

코드로 확인해보자.

1. @Transactional 메서드 호출

public class InternalCallV1Test {
	// 의존성 관련 로직 생략
    @Test
    void printProxy() {
        log.info("callService class={}", callService.getClass());
    }
    
    static class CallService {
    
        @Transactional
        public void internal() {
            log.info("call internal");
            printTxInfo();
        }

        private void printTxInfo() {
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("tx Active={}", txActive);
        }
    }
  • 일단 @Transactional이 붙어있으므로 프록시 객체가 생성된다.
  • 그럼 @Transactional이 붙은 internal메서드를 직접 호출해보면?

    프록시 객체가 호출되므로 트랜잭션이 적용된다.

2. @Transactional 메서드 내부 호출

  • 반대로 서비스 로직에서 internal메서드를 내부 호출하면?
public class InternalCallV1Test {
	// 의존성 관련 로직 생략
    @Test
    void printProxy() {
        log.info("callService class={}", callService.getClass());
    }
    
    @Test
    void externalCall() {
        callService.external();
    }

    static class CallService {

        public void external() {
            log.info("call external");
            printTxInfo();
            // @Transactional 메서드 내부 호출
            internal();
        }

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

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


프록시 객체가 아닌 실제 서비스 객체에서 internal메서드를 호출하므로 트랜잭션이 적용되지 않는다.

그렇다면 이런 문제는 어떻게 해결할 수 있을까?

3. 내부 호출 해결 방법

여러 방법이 있지만 가장 단순한 방법은 @Transactional이 붙은 메서드를 별도의 클래스로 정의하는 것이다.
코드로 확인해보자.

    @RequiredArgsConstructor
    static class CallService {

        private final InternalService internalService;

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

        private void printTxInfo() {
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("tx Active={}", txActive);
        }
    }
	
    // internal 메서드를 별도의 클래스로 분리
    static class InternalService {
        @Transactional
        public void internal() {
            log.info("call internal");
            printTxInfo();
        }

        private void printTxInfo() {
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("tx Active={}", txActive);
        }
    }
  • 앞서 확인했던 코드와 달리 InternalService를 새로 정의하고 여기서 interal메서드를 정의하고 있는 것을 확인할 수 있다. 나머지 코드는 동일하다. 그럼 extenal메서드를 다시 호출해보자.

    internal 메서드가 실행되기 전에 트랜잭션이 적용이 되는 것을 확인할 수 있다. 흐름은 아래와 같다.

참고 1. public에만 트랜잭션 적용

스프링 트랜잭션 기능은 public메서드에만 적용이 된다고 한다.
@Transactional을 클래스 단위에 적용하면 의도하지 않는 메서드에까지 트랜잭션이 적용될 수 있는데, 트랜잭션은 주로 비즈니스 로직의 시작지점에 걸기 때문에 이처럼 외부에 열어준 부분을 시작점으로 한다. 따라서 스프링은 public 메서드에만 트랜잭션을 적용하도록 설정하고 있다.

참고 2. 초기화 시점

  • 스프링 초기화가 진행되는 시점에는 트랜잭션 AOP가 적용되지 않을 수 있다.
    이건 코드로 바로 확인해보자.
    @Slf4j
    static class Hello {
		
        // 트랜잭션 적용 X
        @PostConstruct
        @Transactional
        public void initV1() {
            boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("Hello init @PostConstruct tx active={}", isActive);
        }
		
        // 트랜잭션 적용 O
        @EventListener(ApplicationReadyEvent.class)
        @Transactional
        public void initV2() {
            boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
        }
    }
  1. @PostConstruct와 @Transactional을 함께 사용하는 경우
    트랜잭션이 적용되지 않는다.
    @PostConstruct에 의해 초기화 코드가 먼저 호출이 되고, 그 뒤에 트랜잭션 AOP가 적용되는데, 빈의 생성과 초기화 단계에서는 아직 트랜잭션 AOP 프록시가 생성되지 않았기 때문이다.

  2. @EventListener(ApplicationReadyEvent.class)와 함께 사용
    트랜잭션 AOP를 포함한 스프링 컨테이너가 완전히 생성되고 난 후 메서드가 실행되므로 트랜잭션이 적용된다.

출처 : 김영한 - 스프링 DB 2편

profile
꾸준히 하자!

1개의 댓글

comment-user-thumbnail
2023년 8월 2일

유익한 글이었습니다.

답글 달기