JPA를 기준으로 @Transactional 분석

KIYOUNG KWON·2022년 8월 22일
0
post-custom-banner

@Transactional은 어플리케이션 레벨에서 선언적으로 트랜잭션 처리를 지원한다. 이번 글에선 JPA를 기준으로 @Transactional의 동작에 대해서 알아보도록 하자.

Transactional

import org.springframework.transaction.annotation.Transactional
import javax.transaction.Transactional

우선 @Transactional의 패키지를 살펴보자, java의 확장 패키지나 spring에서 해당 어노테이션을 가져올 수 있다. 두가지 차이점은 버전 호환성과 지원되는 속성값이다. 이번 글에선 두 어노테이션에 대해서 자세히 다루지는 않겠다. spring 4.0 이상이라면 동일하게 동작한다고 생각하면 될 것이다.

Spring Data JPA와 Hibernate

JPA의 Transactional 관련 코드는 Spring Data JPA에서 제공한다. Spring Data JPA는 JPA를 사용하는데 반복적인 코드를 줄이기 위한 boilerplate code를 제공한다. 대표적으로 JpaRepository, JPA에 대한 Transactional 지원 등이 포함된다. 위 그림이 이러한 구조를 가장 잘 표현하고 있다고 생각한다. JPA는 Java에서 ORM을 구현하기 위한 기술의 명세에 가깝다. 구현체는 Hibernate를 주로(99%?) 사용하므로 이 글에서도 Hibernate를 기준으로 분석을 해볼 것 이다.

AnnotationDrivenBeanDefinitionParser

@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
		registerTransactionalEventListenerFactory(parserContext);
		String mode = element.getAttribute("mode");
		if ("aspectj".equals(mode)) {
			// mode="aspectj"
			registerTransactionAspect(element, parserContext);
			if (ClassUtils.isPresent("jakarta.transaction.Transactional", getClass().getClassLoader())) {
				registerJtaTransactionAspect(element, parserContext);
			}
		}
		else {
			// mode="proxy"
            // 보통 여기로 분기한다고 생각하면 됨
			AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext);
		}
		return null;
}

가장 먼저 @Transactional에 대한 XML설정파일을 파싱하는 코드이다. 설정을 읽어 @Transactinal를 위한 Bean들을 초기화 한다. 이 글에선 proxy모드만 살펴볼 것이다.

Spring AOP

@Transactional에 대해서 자세하게 알아보기 전에 먼저 Spring AOP에 대해서 알아보도록 하자. AOP는 Aspect Orient Programming의 약자로 횡단 관심사의 분리를 허용함으로써 모듈성을 증가시키는 것이 목적인 프로그래밍 패러다임이다.[from wiki]

일반적으로 위 그림처럼 프록시 패턴을 사용하여 구현한다.

Java에선 보통 아래 3가지 방식 중 하나를 사용할 수 있다.

  • AspectJ
  • JDK Proxy
  • CGLib

여기서 Spring AOP는 JDK Proxy와 CGlib를 사용한다. Spring AOP는 런타임 시점에 프록시 객체를 생성하여 AOP를 구현한다. Interface가 구현되어 있으면 JDK Proxy, 구현되어 있지 않으면 CGLib를 사용한다. 이 글에선 CGlib를 기준으로 살펴보겠다.

AopAutoProxyConfigurer

/**
* Inner class to just introduce an AOP framework dependency when actually in proxy mode.
*/
private static class AopAutoProxyConfigurer {

	public static void configureAutoProxyCreator(Element element, ParserContext parserContext) {
			AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element);

			String txAdvisorBeanName = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME;
			if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) {
				Object eleSource = parserContext.extractSource(element);

				// Create the TransactionAttributeSource definition.
				RootBeanDefinition sourceDef = new RootBeanDefinition(
						"org.springframework.transaction.annotation.AnnotationTransactionAttributeSource");
				sourceDef.setSource(eleSource);
				sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
				String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef);

				// Create the TransactionInterceptor definition.
				RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class);
				interceptorDef.setSource(eleSource);
				interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
				registerTransactionManager(element, interceptorDef);
				interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName));
				String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef);

				// Create the TransactionAttributeSourceAdvisor definition.
				RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class);
				advisorDef.setSource(eleSource);
				advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
				advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName));
				advisorDef.getPropertyValues().add("adviceBeanName", interceptorName);
				if (element.hasAttribute("order")) {
					advisorDef.getPropertyValues().add("order", element.getAttribute("order"));
				}
				parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef);

				CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource);
				compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName));
				compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName));
				compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, txAdvisorBeanName));
				parserContext.registerComponent(compositeDef);
		}
	}
}

proxy모드로 설정되어 있다면 가장먼저 호출되는 함수로 xml설정값에 따라 아래 3가지 bean을 생성한다.

  • TransactionInterceptor → advice 역할을 수행하는 bean
  • AnnotationTransactionAttributeSource → annotation의 정보를 파싱하는 bean
  • BeanFactoryTransactionAttributeSourceAdvisor → advisor와 pointcut 그리고 annotation정보를 갖고 실제 aop를 등록할 때 사용

BeanFactoryTransactionAttributeSourceAdvisor 내부의 코드를 살펴보면 TransactionAttributeSourcePointcut 타입의 변수를 갖고 있는 것을 확인해 볼 수 있다.

public class BeanFactoryTransactionAttributeSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor { 
// advice관련 정보는 AbstractBeanFactoryPointcutAdvisor에 있음
 @Nullable
	private TransactionAttributeSource transactionAttributeSource; // 어노테이션의 정보
    private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { // pointcut
		@Override
		@Nullable
		protected TransactionAttributeSource getTransactionAttributeSource() {
			return transactionAttributeSource;
		}
	};
 // 생략....
}

여기서 호출 된 함수가 proxy를 수행하는 함수인지 확인 하는 matchs를 확인 할 수 있다.

abstract class org.springframework.transaction.interceptor.TransactionAttributeSourcePointcut
 
  // Determine whether a call on a particular method matches the poincut
  // If it matches then the advice bean will be called
  // The advice bean that has been registered for this pointcut is the
  // TransactionInterceptor class (see Point A)
  public boolean matches(Method method, Class targetClass) {
  TransactionAttributeSource tas = getTransactionAttributeSource();
 
  // Call getTransactionAttribute of the injected transactionAttributeSoirce
  // (see Point C)
  return (tas == null || tas.getTransactionAttribute(method, targetClass) != null);
}

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;
}

@Transactional을 사용해 보았다면 대충 어떤 의미의 코드인지 파악이 될 것이다. @Transactional의 attribute정보를 파싱하고 저장한다. 파싱하는 attribute는 아래와 같다

  • propagation behavior
  • isolation level
  • timeout value for the transaction
  • readOnly flag

TransactionInterceptor

attribute정보를 파싱했으면 실제 트랜잭션 처리를 위해 수행 할 interceptor(프록시를 통해 수행할 동작)를 생성해야 할 것이다. 위 그림은 Spring Data의 @Transactional이 붙은 함수가 실행되면서 거치는 객체(Bean)들이다.

@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();
			}
		});
}

TransactionInterceptor에 invoke라는 함수이다. 실제 하고자 하는 동작이 여기서 시작된다고 생각하면 된다.

TransactionAspectSupport

@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);
	//...
    
	commitTransactionAfterReturning(txInfo);//모든 작업이 끝나고 호출될 함수
}

protected PlatformTransactionManager determineTransactionManager(TransactionAttribute txAttr) {
      //...
}

TransactionInterceptor에 의해 호출되는 함수이다. TransactionAspectSupport는 멤버 변수로 위에서 처리했던 TransactionAttribute와 TransactionManager를 갖고 있어 이를 사용하여 트랜잭션 동작을 처리한다. determineTransactionManager를 통해서 알맞은 TransactionManager를 가져온다. TransactionManager는 AbstractPlatformTransactionManager를 상속받아 사용하는데 AbstractPlatformTransactionManager의 getTransaction함수를 통해 트랜잭션 수행에 필요한 트랜잭션 정보를 가져온다.

JpaTransactionManager

getTransaction에선 doGetTransaction이 호출되는데 이 때 실제 DataSource가 되는 TransactionManager의 override된 doGetTransaction가 호출된다.

@Override
protected Object doGetTransaction() {
		JpaTransactionObject txObject = new JpaTransactionObject();
		txObject.setSavepointAllowed(isNestedTransactionAllowed());

		EntityManagerHolder emHolder = (EntityManagerHolder)
				TransactionSynchronizationManager.getResource(obtainEntityManagerFactory());
		if (emHolder != null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Found thread-bound EntityManager [" + emHolder.getEntityManager() +
						"] for JPA transaction");
			}
			txObject.setEntityManagerHolder(emHolder, false);
		}

		if (getDataSource() != null) {
			ConnectionHolder conHolder = (ConnectionHolder)
					TransactionSynchronizationManager.getResource(getDataSource());
			txObject.setConnectionHolder(conHolder);
		}

		return txObject;
}

JPATransactionManager는 EntityManagerFactory를 TransactionSynchronizationManager로 부터 획득하고 해당 스레드에서 사용할 EntityManager를 가져온다.

JPATransactionManager에 멤버변수인 JPADialect를 사용하여 실제 트랜잭션에 필요한(Hibernate Session같은 객체)작업을 수행한다. 왜냐하면 JPA도 인터페이스 이기 때문에 실제 구현부는 구현체에 따라 다르기 때문이다.

public abstract class TransactionSynchronizationManager {

	private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<>("Transactional resources");

	private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
			new NamedThreadLocal<>("Transaction synchronizations");

	private static final ThreadLocal<String> currentTransactionName =
			new NamedThreadLocal<>("Current transaction name");

	private static final ThreadLocal<Boolean> currentTransactionReadOnly =
			new NamedThreadLocal<>("Current transaction read-only status");

	private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
			new NamedThreadLocal<>("Current transaction isolation level");

	private static final ThreadLocal<Boolean> actualTransactionActive =
			new NamedThreadLocal<>("Actual transaction active");

여기서 트랜잭션에서 중요한 부분이 나온다. 바로 TransactionSynchronizationManager 이다. 이 곳에서 트랜잭션과 관련된 thread local변수들을 관리한다. EntityManagerFactory를 key값으로 thread local에 존재하는 EntityManager를 가져온다, 없으면 생성하는 것으로 보인다.

HibernateJpaDialect

@Override
public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition)
			throws PersistenceException, SQLException, TransactionException {

		SessionImplementor session = getSession(entityManager);

		if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) {
			session.getTransaction().setTimeout(definition.getTimeout());
		}

		boolean isolationLevelNeeded = (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT);
		Integer previousIsolationLevel = null;
		Connection preparedCon = null;

		if (isolationLevelNeeded || definition.isReadOnly()) {
			if (this.prepareConnection && ConnectionReleaseMode.ON_CLOSE.equals(
					session.getJdbcCoordinator().getLogicalConnection().getConnectionHandlingMode().getReleaseMode())) {
				preparedCon = session.getJdbcCoordinator().getLogicalConnection().getPhysicalConnection();
				previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(preparedCon, definition);
			}
			else if (isolationLevelNeeded) {
				throw new InvalidIsolationLevelException(
						"HibernateJpaDialect is not allowed to support custom isolation levels: " +
						"make sure that its 'prepareConnection' flag is on (the default) and that the " +
						"Hibernate connection release mode is set to ON_CLOSE.");
			}
		}

		// Standard JPA transaction begin call for full JPA context setup...
		entityManager.getTransaction().begin();

		// Adapt flush mode and store previous isolation level, if any.
		FlushMode previousFlushMode = prepareFlushMode(session, definition.isReadOnly());
		if (definition instanceof ResourceTransactionDefinition &&
				((ResourceTransactionDefinition) definition).isLocalResource()) {
			// As of 5.1, we explicitly optimize for a transaction-local EntityManager,
			// aligned with native HibernateTransactionManager behavior.
			previousFlushMode = null;
			if (definition.isReadOnly()) {
				session.setDefaultReadOnly(true);
			}
		}
		return new SessionTransactionData(
				session, previousFlushMode, (preparedCon != null), previousIsolationLevel, definition.isReadOnly());
}

HibernateJpaDialect의 beginTransaction함수의 구현부이다. 위 JpaTransactionManager의 부모 class인 AbstractPlatformTransactionManager의 getTransaction의 마지막에 startTransaction에 의해 호출되며 위에서 정의된 정보들을 바탕으로 hibernate session객체를 사용하여 트랜잭션을 시작한다.

결론

실제 소스코드의 모든 부분을 가져오긴 너무 길어서 글에선 언급했지만 없는 함수나 class가 궁금하다면 직접 검색해서 확인해 보길 바란다. 사실 전반적인 흐름만 이해했다면 실무에선 크게 문제될 만한 부분을 없겠지만 @Transactional을 통해 두루뭉실하게 알고있던 Spring AOP의 전반적인 동작과 Bean, thread local에 대해서 구체적이게 파악할 수 있는 유익한 시간이 되었던 것 같다.

post-custom-banner

0개의 댓글