GitHub

https://github.com/Choidongjun0830

Spring

[Spring DB2] 스프링 트랜잭션 이해 - 1

gogi masidda 2024. 3. 27. 18:12
  • 데이터 베이스 접근 기술마다 트랜잭션을 처리하는 방식이 다르다. 그래서 기술을 바꾸면 트랜잭션을 사용하는 코드도 모두 바꿔야 한다. 
  • 스프링은 'PlatformTransactionManager'라는 인터페이스를 통해 트랜잭션 추상화를 제공해주고, 그러면 다른 기술도 동일한 방식으로 사용할 수 있게 된다.
  • 게다가 각 데이터 접근 기술에 대한 트랜잭션 매니저의 구현체도 제공한다. 그래서 개발자가 구현할 일은 없이 잘 가져가다 쓰기만 하면 된다. 
  • 또, 스프링 부트는 어떤 데이터 접근 기술을 사용하는지를 자동으로 인식해서 적절한 트랜잭션 매니저를 선택하여 스프링 빈에 등록해준다. 

트랜잭션 사용방식

  • 선언적 트랜잭션 관리
    • @Transactional
    • 이름 그대로 해당 로직에 트랜잭션을 적용하겠다라고 선언하기만 하면 트랜잭션이 적용된다.
    • 프로그래밍 방식보다 훨씬 간편해서 실무에서 많이 쓰인다. 
  • 프로그래밍 방식 트랜잭션 관리
    • 트랜잭션 매니저나 트랜잭션 템플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성하는 것이다.
    • 애플리케이션 코드가 트랜잭션이라는 기술 코드와 강하게 결합된다. 

트랜잭션 적용 확인

@Slf4j
@SpringBootTest
public class TxBasicTest {

    @Autowired BasicService basicService;

    @Test
    void proxyCheck() {
        log.info("aop class = {}", basicService.getClass());
        assertThat(AopUtils.isAopProxy(basicService)).isTrue();
    }

    @Test
    void txTest() {
        basicService.tx();
        basicService.nonTx();
    }

    @TestConfiguration
    static class TxApplyBasicConfig {
        @Bean
        BasicService basicService() {
            return new BasicService();
        }
    }

    @Slf4j
    static class BasicService {
        //트랜잭션이 적용되는 것
        @Transactional
        public void tx() {
            log.info("call tx");
            //트랜잭션이 이루어지는지 확인
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("tx active = {}", txActive);
        }
        //트랜잭션이 적용안되는 것
        public void nonTx() {
            log.info("call nonTx");
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("tx active = {}", txActive);
        }
    }
}
결과
[springtx] [ Test worker] hello.springtx.apply.TxBasicTest : aop class = class hello.springtx.apply.TxBasicTest$BasicService$$SpringCGLIB$$0
...
[springtx] [ Test worker] o.s.t.i.TransactionInterceptor : Getting transaction for [hello.springtx.apply.TxBasicTest$BasicService.tx]
[springtx] [ Test worker] h.s.apply.TxBasicTest$BasicService : call tx
[springtx] [ Test worker] h.s.apply.TxBasicTest$BasicService : tx active = true
[springtx] [ Test worker] o.s.t.i.TransactionInterceptor : Completing transaction for [hello.springtx.apply.TxBasicTest$BasicService.tx]
[springtx] [ Test worker] h.s.apply.TxBasicTest$BasicService : call nonTx
[springtx] [ Test worker] h.s.apply.TxBasicTest$BasicService : tx active = false
  • @Transactional을 메서드나 클래스에 붙이면, 실제 객체 대신에 트랜잭션을 처리해주는 프록시 객체가 스프링 빈에 등록된다. 주입을 받을 때도, 실제 객체가 아니라 프록시 객체가 주입된다.
    • 이 프록시 객체는 BasicService$$CGLIB이고, BasicService를 상속받는다. 
  • basicService.tx() 호출
    • 클라이언트인 txBasicTest()가 basicService.tx()를 호출하면, tx()가 호출된다. 여기서 프록시는 tx() 메서드가 트랜잭션을 사용할 수 있는지 확인해보는데, tx()에는 @Transactional이 붙어있으므로 트랜잭션이 적용 가능하다.
    • basicService.tx()를 호출하고, 호출이 끝나서 프록시로 제어가 돌아오면 프록시는 트랜잭션 로직을 커밋하거나 롤백해서 트랜잭션을 종료한다.
  • basicService.notTx() 호출
    • 클라이언트인 txBasicTest()가 basicService.nontx()를 호출하면, 트랜잭션 프록시의 nonTx()가 호출된다. 여기서 nonTx() 메서드가 트랜잭션을 사용할 수 있는지 보는데, 여기에는 @Transactional이 안붙어있어서 적용 대상이 아니다. 그래서 트랜잭션을 시작하지 않고, basicService.nonTx()를 호출하고 종료한다.

트랜잭션 적용 위치

@Transactional의 적용 위치에 따른 우선 순위: 스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가진다. 클래스와 메서드 중에서 메서드의 우선순위가 더 높다. 

@SpringBootTest
public class TxLevelTest {

    @Autowired LevelService levelService;

    @Test
    void orderTest() {
        levelService.write();
        levelService.read();
    }

    @TestConfiguration
    static class TxLevelTestConfig {
        @Bean
        LevelService levelService() {
            return new LevelService();
        }
    }
    @Slf4j
    @Transactional(readOnly = true)
    static class LevelService {

        @Transactional(readOnly = false)
        public void write() {
            log.info("call write");
            printTxInfo();
        }

        public void read() {
            log.info("call read");
            printTxInfo();
        }

        private void printTxInfo() {
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("tx active = {}", txActive);
            boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
            log.info("tx readOnly = {}", readOnly);
        }
    }
}
결과
[springtx] [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.apply.TxLevelTest$LevelService.write]
[springtx] [    Test worker] h.s.apply.TxLevelTest$LevelService       : call write
[springtx] [    Test worker] h.s.apply.TxLevelTest$LevelService       : tx active = true
[springtx] [    Test worker] h.s.apply.TxLevelTest$LevelService       : tx readOnly = false
[springtx] [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.apply.TxLevelTest$LevelService.write]
[springtx] [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.apply.TxLevelTest$LevelService.read]
[springtx] [    Test worker] h.s.apply.TxLevelTest$LevelService       : call read
[springtx] [    Test worker] h.s.apply.TxLevelTest$LevelService       : tx active = true
[springtx] [    Test worker] h.s.apply.TxLevelTest$LevelService       : tx readOnly = true
[springtx] [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.apply.TxLevelTest$LevelService.read]
  • LevelService라는 클래스에 @Transactional(readOnly = true)로 옵션을 걸었는데, write() 메서드에 따로 @Transactional(readOnly = false)로 옵션을 걸었다. 더 구체적인 write()에 쓴 것이 적용된다. 
  • write()는 readOnly = false로 나오고, read()는 readOnly = true로 나온다. 

인터페이스에도 @Transactional을 붙일 수 있다. 하지만, 권장하지 않는 방식이다. 

우선순위

  1. 클래스의 메서드
  2. 클래스의 타입
  3. 인터페이스의 메서드
  4. 인터페이스의 타입

트랜잭션 AOP 주의 사항 - 프록시 내부 호출1

  • @Transactional을 사용하면 스프링의 트랜잭션 AOP가 적용된다. 트랜잭션 AOP는 기본적으로 프록시 방식의 AOP를 사용한다.
  • 프록시 객체가 요청을 먼저 받아서 트랜잭션을 처리하고, 실제 객체를 호출해준다. 따라서 트랜잭션을 적용하려면, 항상 프록시를 통해서 대상 객체를 호출해야 한다. 이렇게 해야 프록시에서 먼저 트랜잭션을 적용하고, 대상 객체를 호출하게 된다. 
  • 만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고, 트랜잭션도 적용되지 않는다. 
  • AOP를 적용하면 스프링은 대상 객체 대신에, 대상 객체를 상속받는 프록시 객체를 스프링 빈에 등록한다. 그래서 의존 관계를 주입하면 프록시 객체가 주입된다. 그래서 대상 객체를 직접 호출하는 일은 일반적으로 벌어지지 않는다. 
  • 대상 객체 내부에서 메서드 호출이 발생하면, 프록시를 거치지 않고 대상 객체를 호출하는 문제가 발생한다. 이렇게 되면 @Transactional이 있어도 트랜잭션이 적용되지 않는다. 
@Slf4j
@SpringBootTest
public class InternalCallV1Test {

    @Autowired
    CallService callService; //@Transactional이 있어서 프록시가 주입

    @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 {
        //시나리오는 외부에서 호출이 와서 로직을 수행하고, internal을 가지고 트랜잭션을 수행
        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);
        }
    }


}
internalCall()
[springtx] [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.apply.InternalCallV1Test$CallService.internal]
[springtx] [    Test worker] h.s.a.InternalCallV1Test$CallService     : call internal
[springtx] [    Test worker] h.s.a.InternalCallV1Test$CallService     : tx active = true
[springtx] [    Test worker] h.s.a.InternalCallV1Test$CallService     : tx readOnly = false
[springtx] [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.apply.InternalCallV1Test$CallService.internal]
externalCall() 결과
[springtx] [    Test worker] h.s.a.InternalCallV1Test$CallService     : call external
[springtx] [    Test worker] h.s.a.InternalCallV1Test$CallService     : tx active = false
[springtx] [    Test worker] h.s.a.InternalCallV1Test$CallService     : tx readOnly = false
[springtx] [    Test worker] h.s.a.InternalCallV1Test$CallService     : call internal
[springtx] [    Test worker] h.s.a.InternalCallV1Test$CallService     : tx active = false
[springtx] [    Test worker] h.s.a.InternalCallV1Test$CallService     : tx readOnly = false
  • internalCall()
    • 클라이언트는 callService.internal()을 호출한다. 여기서 callService는 트랜잭션 프록시이다.
    • callService의 트랜잭션 프록시가 호출된다.
    • internal() 메서드에 @Transactional이 붙어 있으므로 트랜잭션 프록시는 트랜잭션을 적용한다.
    • 트랜잭션 적용 후 실제 callService 객체 인스턴스의 internal()을 호출한다.
    • 실제 callService가 처리를 완료하면 응답이 트랜잭션 프록시로 돌아오고, 트랜잭션 프록시는 트랜잭션을 완료한다. 
  • externalCall()
    • 클라이언트는 callService.external()을 호출한다. 여기서 callService는 트랜잭션 프록시이다.
    • callService의 트랜잭션 프록시가 호출된다.
    • external() 메서드에는 @Transactional이 없어서 트랜잭션 프록시는 트랜잭션을 적용하지 않는다.
    • 트랜잭션을 적용하지 않고, 실제 callService 객체 인스턴스의 external()을 호출한다.
    • external()은 내부에서 internal() 메서드를 호출한다. 여기서 문제가 발생한다. 
      • 문제 원인
        • 자바 언어에서 메서드 앞에 별도의 참조가 없으면 this라는 뜻으로 자기 자신의 인스턴스를 가리킨다. 그래서 this.internal()이 호출이 되는데, 여기서 this가 가리키는 것은 실제 대상 객체의 인스턴스를 가리킨다. 그래서 프록시를 거치지 않아 트랜잭션을 적용할 수 없는 것이다. 
        • 프록시 객체에 있는 internal()을 호출한 것이 아니라 실제 대상 객체에 있는 internal()이 호출되는 것이다.
  • 프록시를 사용하면 메서드 내부 호출에 프록시를 적용할 수 없다. 그래서 내부 호출을 피하기 위해 internal()을 별도의 클래스로 분리해야 한다.

트랜잭션 AOP 주의 사항 - 프록시 내부 호출2

internal() 클래스 분

@Slf4j
@SpringBootTest
public class InternalCallV2Test {

    @Autowired
    CallService callService; //@Transactional이 있어서 프록시가 주입

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

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

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

    @Slf4j
    @RequiredArgsConstructor
    static class CallService {

        private final InternalService internalService;

        //시나리오는 외부에서 호출이 와서 로직을 수행하고, internal을 가지고 트랜잭션을 수행
        public void external() {
            log.info("call external");
            printTxInfo();
            internalService.internal(); //외부호출로 바뀜
        }


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

    static class InternalService {
        @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);
        }
    }
}
externalV2() 결과
[springtx] [    Test worker] h.s.a.InternalCallV2Test$CallService     : call external
[springtx] [    Test worker] h.s.a.InternalCallV2Test$CallService     : tx active = false
[springtx] [    Test worker] h.s.a.InternalCallV2Test$CallService     : tx readOnly = false
[springtx] [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.apply.InternalCallV2Test$InternalService.internal]
[springtx] [    Test worker] h.springtx.apply.InternalCallV2Test      : call internal
[springtx] [    Test worker] h.springtx.apply.InternalCallV2Test      : tx active = true
[springtx] [    Test worker] h.springtx.apply.InternalCallV2Test      : tx readOnly = false
[springtx] [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.apply.InternalCallV2Test$InternalService.internal]
  • 클라이언트인 테스트 코드는 callService.external()을 호출
  • callService는 실제 callService의 객체 인스턴스
  • callService는 주입 받은 internalService.internal()을 호출
  • internalService는 트랜잭션 프록시로, internal() 메서드에 @Transactional이 붙어 있어서 트랜잭션 프록시는 트랜잭션을 적용한다.
  • 트랜잭션 적용 후에 트랜잭션 프록시는 실제 객체 인스턴스의 internal()을 호출한다. 

스프링 트랜잭션 AOP는 public 메서드에만 트랜잭션을 적용한다.


트랜잭션 AOP 주의 사항 - 초기화 시점

초기화 시점에 트랜잭션은 적용되지 않는다. 

초기화 코드가 먼저 호출되고, 트랜잭션 AOP가 나중에 적용되기 때문이다.

@SpringBootTest
public class InitTxTest {
    @Autowired
    Hello hello;

    @Test
    void go() {
        //초기화 코드는 스프링이 초기화 시점에 호출한다.
    }

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


    @Slf4j
    static class Hello {
        @PostConstruct
        @Transactional
        public void initV1() {
            boolean active = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("Hello init @PostConstruct tx active = {}", active);
        }
        @EventListener(ApplicationReadyEvent.class) //스프링 컨테이너가 완전히 다 뜨면 호출
        @Transactional
        public void initV2() {
            boolean active = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("Hello init ApplicationReadyEvent tx active = {}", active);
        }
    }
}
  • 초기화 중인 initV1()에서는 트랜잭션이 적용되지 않고, 스프링 컨테이너가 완전히 다 뜬 이후인 initV2()에서는 트랜잭션이 적용된다. 
728x90