TransactionAutoConfiguration 그 후로...

YoonJuHo·2025년 4월 16일
post-thumbnail

오늘의 목표

스프링에서는 어떻게 @Transactional 하나로 트랜잭션이 적용될 수 있는지 전반적인 과정에 대해 알아보겠습니다.

자동구성..근데-이제-Data-JPA를-곁들인 을 통해 Data JPA를 사용하게 되었을 때 어떠한 자동구성이 일어나게 되는지를 확인하면서 마지막에 TransactionAutoConfiguration을 잠깐 다루었습니다.
TransactionAutoConfiguration을 통해 트랜잭션 관리를 위한 AOP 설정 및 인프라가 구성이 되는데, 해당 글은 TransactionAutoConfiguration에서부터 시작해 스프링에서 어떻게 @Transactional 하나로 트랜잭션이 적용될 수 있는지 전반적인 과정에 대해 알아보겠습니다.

TransactionAutoConfiguration

TransactionAutoConfiguration의 내부 코드를 자세히 살펴보면 아래와 같이 구성되어 있습니다.

// 트랜잭션 자동 구성 클래스
@AutoConfiguration // Spring Boot의 자동 구성 클래스임을 선언
@ConditionalOnClass(PlatformTransactionManager.class) // Classpath에 트랜잭션 관련 클래스가 있을 경우에만 활성화
public class TransactionAutoConfiguration {

	/**
	 * 리액티브 트랜잭션 처리용 빈 등록
	 * - ReactiveTransactionManager가 하나만 있을 경우, TransactionalOperator 빈을 자동 등록
	 */
	@Bean
	@ConditionalOnMissingBean // 이미 동일한 타입의 빈이 없는 경우에만 등록
	@ConditionalOnSingleCandidate(ReactiveTransactionManager.class) // 후보가 하나일 경우에만 등록
	public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) {
		return TransactionalOperator.create(transactionManager);
	}

	/**
	 * 일반 트랜잭션을 위한 TransactionTemplate 구성
	 * - PlatformTransactionManager가 단일 후보로 있을 경우 적용
	 */
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnSingleCandidate(PlatformTransactionManager.class)
	public static class TransactionTemplateConfiguration {

		/**
		 * TransactionOperations 빈이 없는 경우 TransactionTemplate 등록
		 * - 명시적 트랜잭션 처리 시 사용됨
		 */
		@Bean
		@ConditionalOnMissingBean(TransactionOperations.class)
		public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
			return new TransactionTemplate(transactionManager);
		}

	}

	/**
	 * 트랜잭션 AOP (프록시 기반 @Transactional) 활성화 구성
	 * - 트랜잭션 매니저가 있고, 수동으로 @EnableTransactionManagement를 안 붙였을 경우에만 적용
	 */
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnBean(TransactionManager.class) // 트랜잭션 매니저가 있을 때만 적용
	@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class) // 개발자가 수동 설정하지 않았을 경우
	public static class EnableTransactionManagementConfiguration {

		/**
		 * JDK 동적 프록시 방식 (@EnableTransactionManagement(proxyTargetClass = false))
		 * - 인터페이스 기반 프록시
		 * - spring.aop.proxy-target-class=false일 경우 적용
		 */
		@Configuration(proxyBeanMethods = false)
		@EnableTransactionManagement(proxyTargetClass = false)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
		public static class JdkDynamicAutoProxyConfiguration {
			// 설정용 빈만 존재, 실제 로직은 없음
		}

		/**
		 * CGLIB 기반 클래스 프록시 (@EnableTransactionManagement(proxyTargetClass = true))
		 * - 클래스 기반 프록시
		 * - 기본값 (matchIfMissing = true)
		 */
		@Configuration(proxyBeanMethods = false)
		@EnableTransactionManagement(proxyTargetClass = true)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
				matchIfMissing = true)
		public static class CglibAutoProxyConfiguration {
			// 설정용 빈만 존재, 실제 로직은 없음
		}

	}

	/**
	 * AspectJ 트랜잭션 처리 지원
	 * - @Transactional을 AspectJ 방식으로 사용하는 경우
	 * - AbstractTransactionAspect 빈이 존재해야 적용됨
	 */
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnBean(AbstractTransactionAspect.class)
	static class AspectJTransactionManagementConfiguration {

		/**
		 * AbstractTransactionAspect는 지연 초기화 대상에서 제외해야 함 (즉시 초기화 필요)
		 */
		@Bean
		static LazyInitializationExcludeFilter eagerTransactionAspect() {
			return LazyInitializationExcludeFilter.forBeanTypes(AbstractTransactionAspect.class);
		}

	}

}

TransactionAutoConfiguration은 Spring Boot에서 @EnableTransactionManagement를 자동으로 등록해주는 역할을 합니다.
즉, 스프링 부트 환경이 아니면 개발자가 직접 @EnableTransactionManagement를 붙여야 합니다.

TransactionAutoConfiguration은 자동 등록의 트리거일 뿐
여기서 주의깊게 보아야할 부분은 TransactionTemplateConfigurationEnableTransactionManagementConfiguration 입니다.

TransactionTemplateConfiguration

TransactionTemplate은 Spring에서 명시적 프로그래밍 방식으로 트랜잭션을 관리할 수 있게 해주는 템플릿 클래스입니다. 보통 @Transactional은 선언적 방식인데, 이건 프로그래밍 방식으로 트랜잭션을 제어하고자 할 때 사용합니다.

// 사용 예시
TransactionTemplate txTemplate = new TransactionTemplate(transactionManager);
String result = txTemplate.execute(status -> {
    // 트랜잭션 안에서 실행할 코드
    someRepository.save(...);
    return "성공";
});

초기에는 트랜잭션을 사용하기 위해 PlatformTransactionManager를 직접 호출해 트랜잭션을 시작하고, 성공 시 커밋하거나 예외 발생 시 롤백하는 코드를 작성해야 했습니다. 이로 인해 동일한 트랜잭션 제어 로직이 여러 클래스에서 반복적으로 사용되는 문제가 발생했습니다.

이를 해결하기 위해 TransactionTemplate이 도입되었습니다. TransactionTemplate을 사용하면 템플릿 콜백 패턴을 통해 트랜잭션 처리 코드를 일관되게 작성할 수 있고, 트랜잭션 시작, 커밋, 롤백 로직을 템플릿이 대신 처리해줍니다. 이 덕분에 반복되는 트랜잭션 제어 코드를 제거할 수 있게 되었습니다.

하지만 여전히 문제는 남아 있었습니다. TransactionTemplate을 사용하는 방식에서도 비즈니스 로직과 트랜잭션 처리 로직이 같은 클래스 안에 섞여 있어 두 관심사를 하나의 클래스에서 처리하게 되므로 관심사 분리가 어렵고 유지보수가 불편하다는 점입니다.

이 문제를 해결하기 위해 스프링은 AOP 기반의 선언적 트랜잭션 처리 방식을 제공합니다. @Transactional 어노테이션을 사용하면, 트랜잭션 경계 설정을 AOP 프록시가 대신 처리해주기 때문에, 개발자는 오직 비즈니스 로직에만 집중할 수 있습니다. 트랜잭션의 시작과 종료, 롤백 여부 등은 AOP 프레임워크가 자동으로 처리합니다.

Q. 만약 @Transactional을 사용한다면 TransactionTemplate을 사용하지 않는거네?

맞습니다. @Transactional 을 사용하게 된다면 AOP 기반의 트랜잭션 처리를 이용하는 것으로 TransactionTemplate은 전혀 등장하지 않습니다.
대신 TransactionInterceptor 가 메서드 호출을 가로채서 기존에 TransactionTemplate이 수행했던 트랜잭션 시작, 커밋, 롤백 로직을 대신 수행해주게 됩니다.

EnableTransactionManagementConfiguration

해당 클래스는 Spring Boot에서 @EnableTransactionManagement를 자동 설정하는 자동 구성 클래스입니다. 구체적으로는, AOP 기반 트랜잭션 처리에서 JDK 동적 프록시(JDK Proxy)와 CGLIB 프록시 중 어떤 것을 사용할지 자동으로 설정해주는 역할을 합니다.

@EnableTransactionManagement...?

@EnableTransactionManagement는 Spring에서 @Transactional이 실제로 동작하도록 활성화해주는 어노테이션입니다. -> 트랜잭션 AOP 설정을 활성화하는 역할 (환경 세팅)

  • @EnableTransactionManagement 없으면 @Transactional은 무시됨 (프록시가 안 만들어짐)
  • @EnableTransactionManagement만 있고 @Transactional이 없으면? → 아무 효과 없음

둘 다 있어야 AOP 기반 트랜잭션 처리가 정상적으로 작동

이 어노테이션이 적용되면 Spring은 TransactionManagementConfigurationSelector를 통해 다음 두 Bean을 등록합니다

  • AutoProxyRegistrar: AOP 프록시 인프라 구성
  • ProxyTransactionManagementConfiguration: Transaction Advisor와 Interceptor 등록

AutoProxyRegistrar...? -> 빈 후처리기를 컨테이너에 등록하는 역할

이 클래스는 InfrastructureAdvisorAutoProxyCreator를 빈으로 등록합니다. 이 빈(InfrastructureAdvisorAutoProxyCreator)은 @Transactional이 붙은 메서드를 자동으로 프록시로 감싸줍니다 (즉, 동작을 가로채기 위한 프록시 생성)
즉, AutoProxyRegistrar 클래스는 @EnableTransactionManagement, @EnableAsync, @EnableAspectJAutoProxy
AOP 기반 기능을 활성화하는 어노테이션들에 의해 등록되는 클래스입니다.

이 클래스의 역할은 AOP 기능을 수행할 AutoProxyCreator 빈을 등록하는 것입니다.
즉, 실제 AOP를 수행하는 객체는 아니고, 프록시를 생성해줄 객체(AutoProxyCreator)를 스프링 컨테이너에 등록해주는 역할을 합니다.

내부에서는..

  • @EnableTransactionManagement → ImportSelector로 AutoProxyRegistrar를 import
  • AutoProxyRegistrar.registerBeanDefinitions()에서 InfrastructureAdvisorAutoProxyCreator(=AutoProxyCreator) 같은 프록시 생성기 빈을 BeanDefinitionRegistry에 등록 (아래 사진 참고)

이후 Spring이 빈을 생성할 때 이 AutoProxyCreator가 개입해서 프록시 객체(AOP 대상)를 감싸도록 합니다.

그럼 진짜 프록시를 만드는 주체는?

  • AutoProxyRegistrar가 아니라 AutoProxyCreator들이 실제 프록시를 생성합니다.

대표적인 AutoProxyCreator

  • InfrastructureAdvisorAutoProxyCreator : @Transactional, @Async 등 인프라 수준의 AOP용
  • AnnotationAwareAspectJAutoProxyCreator : @Aspect 기반 AOP용
  • BeanNameAutoProxyCreator : 빈 이름으로 지정된 AOP 대상 프록시 생성

AnnotationAwareAspectJAutoProxyCreator는 @Aspect 기반 AOP를 처리하기 위한 빈 후처리기 (BeanPostProcessor).
그리고 이건 우리가 흔히 말하는 커스텀 AOP (예: 로깅, 인증 체크, 성능 측정 등) 에서 동작하는 핵심 컴포넌트.

결국은 AutoProxyRegistrar을 통해 AOP 프록시 생성기가 생성된다는 것을 알았습니다.
정리하자면

  • AutoProxyRegistrar 클래스는 InfrastructureAdvisorAutoProxyCreator를 빈으로 등록
  • InfrastructureAdvisorAutoProxyCreator@Transactional이 붙은 메서드를 자동으로 프록시로 감싸줌

ProxyTransactionManagementConfiguration...?

ProxyTransactionManagementConfiguration는 트랜잭션을 위한 AdvisorInterceptor 등록하는 역할을 수행합니다.

  • TransactionInterceptor (트랜잭션 로직을 수행)
  • TransactionAttributeSourceAdvisor (Advisor이며 Pointcut + Advice 형태)
    이 Advisor는 @Transactional 어노테이션을 인식

최종적으로 @EnableTransactionManagement에 의해 등록된 AutoProxyRegistrarProxyTransactionManagementConfiguration을 통해 @Transactional이 붙은 메서드는 프록시로 감싸지게 됩니다.
이 프록시는 내부적으로 TransactionInterceptor를 호출합니다.

TransactionAspectSupport (=TransactionInterceptor의 부모 클래스)

TransactionInterceptorinvoke 메서드를 호출하게 되는데,

TransactionInterceptor는 상속받은 TransactionAspectSupportinvokeWithinTransaction() 메서드를 통해 트랜잭션을 시작하고, 커밋하거나 롤백하는 과정을 수행합니다.

Spring에서 @Transactional이 붙은 메서드가 실행될 때, 트랜잭션이 실제로 필요한 경우 트랜잭션을 생성하고 정보 객체를 구성하는 핵심 메서드가 바로 TransactionAspectSupportcreateTransactionIfNecessary()입니다.

이 메서드는 트랜잭션 처리 흐름의 진입부인 invokeWithinTransaction() 내부에서 호출됩니다.


    protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
        if (txAttr != null && ((TransactionAttribute)txAttr).getName() == null) {
            txAttr = new DelegatingTransactionAttribute((TransactionAttribute)txAttr) {
                public String getName() {
                    return joinpointIdentification;
                }
            };
        }

        TransactionStatus status = null;
        if (txAttr != null) {
            if (tm != null) {
                status = tm.getTransaction((TransactionDefinition)txAttr);
            } else if (this.logger.isDebugEnabled()) {
                this.logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + "] because no transaction manager has been configured");
            }
        }

        return this.prepareTransactionInfo(tm, (TransactionAttribute)txAttr, joinpointIdentification, status);
    }

큰 흐름을 정리하면 아래와 같습니다.

스프링 선언적 트랜잭션(@Transactional)의 큰 흐름


@EnableTransactionManagement (자동 적용됨)TransactionManagementConfigurationSelector
    ↓
 ┌────────────────────────┬────────────────────────────┐
 │ AutoProxyRegistrarProxyTransactionConfig     │
 │ → AOP Creator 등록     │ → TransactionInterceptor   │
 │                        │ → TransactionAdvisor       │
 └────────────────────────┴────────────────────────────┘
    ↓
@MyService 등록됨 → 이때 @Transactional 프록시로 감쌈
    ↓
MyService.method() 호출 → 프록시의 invoke()TransactionInterceptorinvokeWithinTransaction()PlatformTransactionManagergetTransaction() / commit() / rollback()

이렇게 해서 TransactionAutoConfiguration 부터 시작해서 TransactionInterceptor까지의 과정을 통해 스프링 내부에서 선언적 트랜잭션이 적용되는 전반적인 과정을 알 수 있었습니다. 다음에는 PlatformTransactionManager의 동작 방식에 대해 알아보겠습니다.

아래는 자동구성..근데-이제-Data-JPA를-곁들인 부터 시작해서 지금까지의 흐름을 정리한 표입니다.

🔄 Spring Boot 컨테이너 라이프사이클 + @Transactional 적용 흐름 정리

단계설명관련 클래스 또는 어노테이션생명주기 타이밍
0️⃣애플리케이션 시작, SpringApplication.run() 호출SpringApplication애플리케이션 부트
1️⃣사용자 정의 @Component, @Service 등의 스캔 시작@ComponentScan, ConfigurationClassPostProcessor빈 정의 등록 시작 전
2️⃣사용자 정의 빈 구성 클래스(@Configuration 등) 먼저 처리됨@Configuration, @Bean등록 우선순위 ↑
3️⃣@EnableAutoConfiguration에 따라 AutoConfig 클래스 로딩spring.factoriesMETA-INF자동 구성 클래스 탐색 시점
4️⃣DataSourceAutoConfiguration 실행 → HikariDataSource 등록DataSourceAutoConfigurationDB 연결 구성
5️⃣HibernateJpaAutoConfiguration 실행JpaVendorAdapter, EntityManagerFactoryBuilderDataSource 이후
HibernateJpaConfiguration 로드LocalContainerEntityManagerFactoryBean
JpaTransactionManager, EntityManagerFactory 등록JpaBaseConfiguration 상속
6️⃣JpaRepositoriesAutoConfiguration 실행Repository 스캔Hibernate 이후
7️⃣TransactionAutoConfiguration 실행트랜잭션 관련 설정DataSource, JPA 이후
EnableTransactionManagementConfiguration 내부에서
@EnableTransactionManagement 자동 적용중요 트리거
TransactionManagementConfigurationSelector → Import 두 개
AutoProxyRegistrarInfrastructureAdvisorAutoProxyCreator 등록AOP 프록시 생성기
ProxyTransactionManagementConfigurationTransactionInterceptor, TransactionAdvisor 등록어드바이저 구성 완료
8️⃣이 시점에 @Service, @Component 등 사용자 정의 빈 등록 시작일반적인 빈 생성 시점
9️⃣등록된 BeanPostProcessor 동작 시작InfrastructureAdvisorAutoProxyCreator
Advisor 조건 만족 시 프록시 감싸짐 (@Transactional 등)프록시 객체 생성
🔟사용자 코드에서 메서드 호출 시 → 프록시 객체가 가로채기
TransactionInterceptor.invoke() 실행됨
내부적으로 invokeWithinTransaction() 호출트랜잭션 시작, 커밋, 롤백 처리
PlatformTransactionManager.getTransaction() 호출트랜잭션 생성 여부 판단
invocation.proceedWithInvocation()실제 비즈니스 로직 실행
정상 종료 시 commitTransactionAfterReturning() 실행PlatformTransactionManager.commit() 호출
예외 발생 시 completeTransactionAfterThrowing() 실행PlatformTransactionManager.rollback() 호출

🔄 Spring Bean Lifecycle + @Transactional 적용 지점 정리

라이프사이클 단계설명기존 흐름에서 해당하는 단계
1️⃣ 빈 정의 등록 (Definition 등록)어떤 빈이 존재할 것인지 스프링이 "정의"만 먼저 등록하는 단계① ~ ⑦ 전체
- @ComponentScan
- @EnableAutoConfiguration
- AutoProxyRegistrar, TransactionInterceptor 등도 이 시점 등록
2️⃣ 빈 인스턴스 생성정의된 빈을 바탕으로 객체를 new해서 인스턴스를 만드는 단계⑧ 사용자 정의 빈 생성 시점
예: @Service, @Component 클래스
3️⃣ 의존성 주입생성된 빈에 필요한 의존 객체를 @Autowired, 생성자 등으로 주입⑧과 함께 진행
4️⃣ 초기화 (PostProcessor 포함)InitializingBean.afterPropertiesSet(), @PostConstruct
+ BeanPostProcessor 적용
→ 이 시점에 프록시 감싸짐

- InfrastructureAdvisorAutoProxyCreator 작동
- @Transactional 조건 만족 시 프록시로 감쌈
5️⃣ 사용사용자가 메서드 호출 → 프록시가 가로채서 트랜잭션 처리 시작🔟 이후
- TransactionInterceptor.invoke()
- invokeWithinTransaction() 내부에서 트랜잭션 시작, 커밋, 롤백
6️⃣ 소멸빈이 컨테이너에서 제거될 때 실행
@PreDestroy, DisposableBean.destroy()
트랜잭션과는 직접적인 관련 없음
  • @Transactional의 실제 적용 타이밍은 빈 초기화 직전인 BeanPostProcessor 단계에서 발생.
  • 즉, 빈이 생성된 후에 프록시로 감싸질 수 있는지 조건 판단 → 감싸기가 이뤄짐.
  • 감싸진 후, 메서드가 호출될 때 트랜잭션이 동작하고, 이건 Bean 사용 단계에 해당.

0개의 댓글