ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • @Transactional vs TransactionTemplate
    극락코딩 2023. 8. 22. 00:53

    스프링에서 트랜잭션을 다루는 2가지 방법이 있다. @Transactional을 사용하는 선언적 트랜잭션과 TransactionTemplate을 사용하는 프로그래밍적 트랜잭션이다.

     

    2가지 방법에 대해 간단하게 정리하면, 다음과 같다.

     

    @Transactional

    • 어노테이션 기반으로 method에 트랜잭션을 거는 방법
    • proxy로 원래 객체를 감싸는데, 그 이유는 실제 메서드의 가장 앞단과 가장 뒤에 트랜잭션의 begin과 commit으로 감싸기 위함

     

    TransactionInterceptor.class

    @Override
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
       // Work out the target class: may be {@code null}.
       // The TransactionAttributeSource should be passed the target class
       // as well as the method, which may be from an interface.
       Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    
       // Adapt to TransactionAspectSupport's invokeWithinTransaction...
       return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
          @Override
          @Nullable
          public Object proceedWithInvocation() throws Throwable {
             return invocation.proceed();
          }
          @Override
          public Object getTarget() {
             return invocation.getThis();
          }
          @Override
          public Object[] getArguments() {
             return invocation.getArguments();
          }
       });
    }

    proxy를 타면서, 해당 메서드가 실제 트랜잭션 메서드인지 파악

     

     

    TransactionAspectSupport.class

    @Nullable
    protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
          final InvocationCallback invocation) throws Throwable {
    
       // If the transaction attribute is null, the method is non-transactional.
       TransactionAttributeSource tas = getTransactionAttributeSource();
       final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
       final TransactionManager tm = determineTransactionManager(txAttr);
    
       if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
          boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
          boolean hasSuspendingFlowReturnType = isSuspendingFunction &&
                COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName());
          if (isSuspendingFunction && !(invocation instanceof CoroutinesInvocationCallback)) {
             throw new IllegalStateException("Coroutines invocation not supported: " + method);
          }
          CoroutinesInvocationCallback corInv = (isSuspendingFunction ? (CoroutinesInvocationCallback) invocation : null);
    
          ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
             Class<?> reactiveType =
                   (isSuspendingFunction ? (hasSuspendingFlowReturnType ? Flux.class : Mono.class) : method.getReturnType());
             ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType);
             if (adapter == null) {
                throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
                      method.getReturnType());
             }
             return new ReactiveTransactionSupport(adapter);
          });
    
          InvocationCallback callback = invocation;
          if (corInv != null) {
             callback = () -> CoroutinesUtils.invokeSuspendingFunction(method, corInv.getTarget(), corInv.getArguments());
          }
          Object result = txSupport.invokeWithinTransaction(method, targetClass, callback, txAttr, (ReactiveTransactionManager) tm);
          if (corInv != null) {
             Publisher<?> pr = (Publisher<?>) result;
             return (hasSuspendingFlowReturnType ? KotlinDelegate.asFlow(pr) :
                   KotlinDelegate.awaitSingleOrNull(pr, corInv.getContinuation()));
          }
          return result;
       }
    
       PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
       final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
    
       if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
          // Standard transaction demarcation with getTransaction and commit/rollback calls.
          TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
    
          Object retVal;
          try {
             // This is an around advice: Invoke the next interceptor in the chain.
             // This will normally result in a target object being invoked.
             retVal = invocation.proceedWithInvocation();
          }
          catch (Throwable ex) {
             // target invocation exception
             completeTransactionAfterThrowing(txInfo, ex);
             throw ex;
          }
          finally {
             cleanupTransactionInfo(txInfo);
          }
    
          if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
             // Set rollback-only in case of Vavr failure matching our rollback rules...
             TransactionStatus status = txInfo.getTransactionStatus();
             if (status != null && txAttr != null) {
                retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
             }
          }
    
          commitTransactionAfterReturning(txInfo);
          return retVal;
       }
    
       else {
          Object result;
          final ThrowableHolder throwableHolder = new ThrowableHolder();
    
          // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
          try {
             result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
                TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
                try {
                   Object retVal = invocation.proceedWithInvocation();
                   if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
                      // Set rollback-only in case of Vavr failure matching our rollback rules...
                      retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                   }
                   return retVal;
                }
                catch (Throwable ex) {
                   if (txAttr.rollbackOn(ex)) {
                      // A RuntimeException: will lead to a rollback.
                      if (ex instanceof RuntimeException) {
                         throw (RuntimeException) ex;
                      }
                      else {
                         throw new ThrowableHolderException(ex);
                      }
                   }
                   else {
                      // A normal return value: will lead to a commit.
                      throwableHolder.throwable = ex;
                      return null;
                   }
                }
                finally {
                   cleanupTransactionInfo(txInfo);
                }
             });
          }
          catch (ThrowableHolderException ex) {
             throw ex.getCause();
          }
          catch (TransactionSystemException ex2) {
             if (throwableHolder.throwable != null) {
                logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                ex2.initApplicationException(throwableHolder.throwable);
             }
             throw ex2;
          }
          catch (Throwable ex2) {
             if (throwableHolder.throwable != null) {
                logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
             }
             throw ex2;
          }
    
          // Check result state: It might indicate a Throwable to rethrow.
          if (throwableHolder.throwable != null) {
             throw throwableHolder.throwable;
          }
          return result;
       }
    }

    실제 트랜잭션의 로직이 수행되는 메서드

    commit, rollback 등 다 처리

     

     

    SpringTransactionAnnotationParser

    protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) {
       RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute();
    
       Propagation propagation = attributes.getEnum("propagation");
       rbta.setPropagationBehavior(propagation.value());
       Isolation isolation = attributes.getEnum("isolation");
       rbta.setIsolationLevel(isolation.value());
    
       rbta.setTimeout(attributes.getNumber("timeout").intValue());
       String timeoutString = attributes.getString("timeoutString");
       Assert.isTrue(!StringUtils.hasText(timeoutString) || rbta.getTimeout() < 0,
             "Specify 'timeout' or 'timeoutString', not both");
       rbta.setTimeoutString(timeoutString);
    
       rbta.setReadOnly(attributes.getBoolean("readOnly"));
       rbta.setQualifier(attributes.getString("value"));
       rbta.setLabels(Arrays.asList(attributes.getStringArray("label")));
    
       List<RollbackRuleAttribute> rollbackRules = new ArrayList<>();
       for (Class<?> rbRule : attributes.getClassArray("rollbackFor")) {
          rollbackRules.add(new RollbackRuleAttribute(rbRule));
       }
       for (String rbRule : attributes.getStringArray("rollbackForClassName")) {
          rollbackRules.add(new RollbackRuleAttribute(rbRule));
       }
       for (Class<?> rbRule : attributes.getClassArray("noRollbackFor")) {
          rollbackRules.add(new NoRollbackRuleAttribute(rbRule));
       }
       for (String rbRule : attributes.getStringArray("noRollbackForClassName")) {
          rollbackRules.add(new NoRollbackRuleAttribute(rbRule));
       }
       rbta.setRollbackRules(rollbackRules);
    
       return rbta;
    }

    어노테이션에 들어 있는 설정 값을 꺼내와서 반환

     

    더 깊게 들어가면 뇌절이기 때문에, 간단 정리하면

    트랜잭션 어노테이션은 proxy를 이용하여 메서드에 대한 트랜잭션 처리가 가능하도록 함

     

     

     

    TransactionTemplate

    - 프로그래밍적 코드 기반으로 트랜잭션을 걸 수 있는 기능을 제공

    - 프록시를 사용하지 않고 Callback method를 기반으로 트랜잭션 처리 작업 진행

     

    TransactionTemplate.class

    @Override
    @Nullable
    public <T> T execute(TransactionCallback<T> action) throws TransactionException {
       Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
    
       if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
          return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
       }
       else {
          TransactionStatus status = this.transactionManager.getTransaction(this);
          T result;
          try {
             result = action.doInTransaction(status);
          }
          catch (RuntimeException | Error ex) {
             // Transactional code threw application exception -> rollback
             rollbackOnException(status, ex);
             throw ex;
          }
          catch (Throwable ex) {
             // Transactional code threw unexpected exception -> rollback
             rollbackOnException(status, ex);
             throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
          }
          this.transactionManager.commit(status);
          return result;
       }
    }

    TransactionCallBack Function Interface를 매개변수로 받아서, 트랜잭션 처리 작업을 진행한다.

     

     

     

    여기까지 간단한 트랜잭션 개요이고, 작업 도중 발생한 궁금증.

    트랜잭션의 readOnly를 기반으로 master와 slave db를 라우팅하도록 구성이 되어 있다.

    이때, 아래와 같은 코드인 경우 getV1과 getV2는 각각 master와 slave 중에서 어디로 데이터를 조회할까?

     

    class MasterSlaveRoutingDataSource : AbstractRoutingDataSource() {
        override fun determineCurrentLookupKey(): Any {
            if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
                return "SLAVE"
            }
            return "MASTER"
        }
    }
    
    @Repository
    interface TestRepository : JpaRepository<Test, Long> {
        @Transactional(readOnly = true)
        fun findById(id: Long): Test?
    }
    
    @Service
    class TestService(
        private val testRepository: TestRepository,
        private val txTemplate: TransactionTemplate,
    ) {
        fun getV1(id: Long) {
            txTemplate.execute { testRepository.findById(id) }
        }
    
        fun getV2(id: Long) {
            testRepository.findById(id)
        }
    }

     

     

    정답은 getV1은 master를 조회하고, getV2()는 slave를 조회한다.

    이유는 getV2의 경우 txTemplate을 사용하기 때문에, @Transaction(readOnly=true)에 대한 처리작업을 진행하지 않는다.

    call-back function interface를 통해 동작하기 때문이다.

     

    대신, DefaultTransactionDefinition 값을 따라가는데, 다음과 같이 설정되어 있다.

    public static final String PREFIX_PROPAGATION = "PROPAGATION_";
    
    /** Prefix for the isolation constants defined in TransactionDefinition. */
    public static final String PREFIX_ISOLATION = "ISOLATION_";
    
    /** Prefix for transaction timeout values in description strings. */
    public static final String PREFIX_TIMEOUT = "timeout_";
    
    /** Marker for read-only transactions in description strings. */
    public static final String READ_ONLY_MARKER = "readOnly";
    
    
    /** Constants instance for TransactionDefinition. */
    static final Constants constants = new Constants(TransactionDefinition.class);
    
    private int propagationBehavior = PROPAGATION_REQUIRED;
    
    private int isolationLevel = ISOLATION_DEFAULT;
    
    private int timeout = TIMEOUT_DEFAULT;
    
    private boolean readOnly = false;

    마지막 readOnly = false가 default인 것을 파악할 수 있다.. 그렇기 때문에 getV2가 master를 조회한다.

     

     

    반대로 getV2의 경우 @Transaction을 기반으로 프록시를 만들어 처리작업을 진행한다.

     

     

    만약에, getV1도 slave에서 조회할려면 어떻게 해야 할까?

     

    fun getV1(id: Long) {
        txTemplate.apply {
            isReadOnly = true
        }.execute { testRepository.findById(id) }
    }

    txTemplate에 isReadOnly를 설정하게 되면, master가 아닌 slave에서 조회가 가능하다.

     


    결론

    - @Transactional을 사용하여 트랜잭션 처리를 하는 경우 어노테이션의 설정값을 따라간다.

    - TransactionTemplate을 사용하며 트랜잭션을 처리하는 경우, txTemplate의 DefaultTransactionDefinition 값을 기본적으로 따라간다.

    - TransactionTemplate의 excute에 @Transactional(readOnly=true)가 달린 function이 실행이 되더라도, 어노테이션 값을 사용하지 않는다. 정확히는 사용하지 못한다. (어노테이션을 읽을 수 있는 aspect가 없어요, aop, proxy xxx)

    '극락코딩' 카테고리의 다른 글

    API Latency를 줄이는 방법 (Part. 0)  (0) 2023.08.28
    Slack Message 발송  (1) 2023.08.27
    온디맨드가 뭔디?  (1) 2023.08.20
    Redis Pub-Sub 사용하기  (0) 2023.08.19
    디프만 똑스 서버에서 캐싱 공통 모듈 만들기  (0) 2023.08.17
극락코딩