본문 바로가기

Spring

Spring @Transactional 동작원리 완벽 정리

들어가면서

Spring을 이용해 개발하다보면 누구나 한 번은 @Transactional을 사용해 본 경험이 있을 것이다. 나 또한 애플리케이션을 개발하며 @Transactional을 굉장히 많이 사용한 경험이 있다.

 

그러다 문득 @Transactional의 동작원리를 제대로 이해하지 않고 사용하고 있다는 생각이 들었다. 어렴풋이 @Transactional을 사용하면 비지니스 로직중 예상하지 못한 문제로 예외가 발생했을때 데이터 정합성을 보장하기 위해 그동안 수정된 데이터를 원복한다고만 이해하고 있었다. 그래서 이번기회에 @Transactional이 내부적으로 어떻게 동작하는지 이해하고 싶어 Spring 코드를 뜯어봤다. 이번 글에서는 Spring 코드를 뜯어보면서 이해한 내용을 정리해 공유하려고 한다.

 

Transaction 워밍업

Transaction은 많은 의미를 담고 있는 단어다. 하지만 컴퓨터 분야에서는 주로 다음의 정의로 사용되는 것 같다.

a unit of work performed

 

작업의 한 단위로, 작업에 포함된 테스크들이 모두 완료되거나 모두 완료되지 않는 것을 의미한다.

 

이를 좀 더 풀어 설명하자면 은행 이체로 이야기 해볼 수 있다. (가장 많이 사용되는 예시이다.)

예를들어 A가 B에게 100만원을 송금하면 다음의 테스크들이 수행되어야 한다.

1. A의 계좌에서 100만원이 빠져나가는 테스크

2. B의 계좌에 100만원을 추가하는 테스크

두 테스크는 모두 수행되거나, 수행 중 이슈가 생긴다면 둘 다 수행되지 않아야한다. 이런식으로 모두 수행되거나 수행되지 않아야 하는 작업 단위를 transaction이라고 한다.

 

Spring에서 transaction 사용하기

(소스코드 원본은 GitHub을 참조하기 바란다.)

 

Spring 프로젝트에서 transaction을 사용하기 위해서 아래와 같이 정말 간단한 코드만 추가하면 된다. (나는 spring-data-jpa 모듈을 사용중이다.)

// import 생략..

@Service
@RequiredArgsConstructor
public class CustomerService {

    private final CustomerRepository customerRepository;

    @Transactional
    public Customer saveCustomer(Customer customer) {
        return customerRepository.save(customer);
    }
}

 

위 코드는 @Transactional 애노테이션을 붙여 saveCustomer 메서드에서 일어나는 작업에 트랜젝션을 걸어주는 코드이다.

 

Spring Boot를 사용한다면 특별한 설정을 하지 않아도 @Transactional 애노테이션을 이용해 내가 원하는 로직에 트랜젝션을 걸 수 있다. 하지만 이해를 위해 @transactional에 필요한 빈들을 직접 등록해보겠다.

 

뒤에서 자세히 설명하겠지만, @Transactional을 이용하면 AOP를 통해 transaction이 동작한다. 그리고 이때 PlatformTransactionalManager 타입의 빈을 이용해 트랙젝션을 동작시킨다. 따라서 먼저 알맞은 PlatformTransactionalManager 구현체를 Spring Container에 등록해줘야 한다.

   @Bean
   public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactory) {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory.getObject());
        return transactionManager;
   }

 

코드에 대해 설명하기 전에 PlatformTransactionManager에 대해 간략하게 설명하고 넘아가겠다. 공식문서를 보면 아래와 같이 나온다.

This is the central interface in Spring's imperative transaction infrastructure. Applications can use this directly, but it is not primarily meant as an API: Typically, applications will work with either TransactionTemplate or declarative transaction demarcation through AOP.

 

PlatformTransactionManager는 Spring의 transaction 인프라에서 중심적인 역할을 하는 인터페이스이다. 

 

그럼 다시 본문으로 넘어와 코드를 설명하겠다.

나는 PlatformTransactionManager 구현체로 JpaTransactionManager를 사용했다. (물론 다른 구현체를 사용해도 된다.)

이대로 끝나면 좋겠지만.. 코드를 보면 알 수 있듯이 JpaTransactionManager에는 EntityManager 객체를 만들어주는 EntityManagerFactory 객체를 설정해줘야 한다.

 

EntityManagerFactory 객체를 생성해 Spring Container에 등록하겠다.

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
	LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
	emf.setDataSource(dataSource);
	emf.setPackagesToScan("com.example.playgroundspring.transactional");
	emf.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
	emf.setJpaProperties(additionalProperties());
	return emf;
}

 

위 코드에서 알 수 있듯이 EntityManagerFactory를 만들기 위해서는 적절한 DataSource를 설정해줘야 한다.

 

DataSource는 내가 직접 정의하지 않고, 기본적으로 등록되는 DataSource를 이용하겠다.

@Autowired
private DataSource dataSource;

 

지금까지 @Transactional을 위해 여러 타입의 빈들을 Spring Container에 등록했다. 이를 시각화 하면 다음과 같다.

구조도

EntityManager : Interface used to interact with the persistence context.

DataSource : A factory for connections to the physical data source that this DataSource object represents

 

Spring @Transactional 코드 뜯어보기

위에서 Spring에서 transaction을 사용하기 위해 필요한 Bean들에 대해 알아보았다. 이번에는 실제로 @Transactional을 이용했을 때 어떻게 동작 되는지 알아보겠다.

 

(디버거를 이용해 아래 코드를 실행하면서 코드 흐름을 정리했다.)

 

@Transactional이 적용된 메소드

@Transactional
public Customer saveCustomerWithException(Customer customer) {
	Customer savedCustomer = customerRepository.save(customer);

	if (Objects.nonNull(savedCustomer.getId())) {
		throw new RuntimeException();
	}

	return savedCustomer;
}

 

테스트 코드

@Test
void test2() {
	Customer newCustomer = Customer.builder()
		.firstName("hello")
		.lastName("world")
		.build();

	try {
		customerService.saveCustomerWithException(newCustomer);
	} catch (RuntimeException e) {
		System.out.println("예외 발생해 rollback 함.");
	}
        
	List<Customer> allCustomers = customerService.findAllCustomers();
	assertThat(allCustomers).isEmpty();
}

 

 

@Transactional을 Spring AOP를 이용해 동작한다. (AOP에 대한 자세한 내용은 공식문서를 참조하기 바란다.)

실제 transaction 로직은 TransactionInterceptor 클래스에서 책임진다.

TransactionInterceptor.invoke

위 코드를 보면 실제 CustomerService 객체의 saveCustomerWithException 메소드가 실행되기 전에 TransactionInterceptor의 로직이 실행됨을 볼 수 있다. 핵심 로직은 invokeWithTransaction 메소드에서 실행된다.

 

invokeWithTransaction 메소드에서는 여러 데이터를 이용해 어떤 TransactionManager를 사용할지 결정한다.

TransactionAspectSupport.invokeWithTransaction

 

코드를 좀 더 따라가보면 핵심적인 부분을 확인할 수 있다.

TransactionAspectSupport.invokeWithTransaction

실제로 transaction을 시작하고 비지니스 로직(saveCustomerWithException)을 수행한다. 그리고 비지로직 수행 중 예외가 발생하면 예외를 잡아 completeTransactionAfterThrowing 메서드를 수행한다. 실제로 rollback 로직이 있는지 좀 더 따라 들어가겠다.

 

TransactionAspectSupport.completeTransactionAfterThrowing

코드에서 볼 수 있듯이 실제로 롤백이 수행된다.

 

지금까지 살펴본 흐름을 간략하게 시각화하면 다음과 같다.

 

 

마무리

이번 글을 통해 Spring에서 @Transactional을 사용하기 위해서는 어떤 빈들이 필요한지, 실제로 어떻게 동작하는지 알아봤다. 이 글을 통해 @Transactional의 원리를 이해하고 실제로 문제를 만났을때  쉽게 해결하길 바란다.