@Transactional은 Spring AOP를 기반으로 동작하는 프락시이기 때문에 AOP에 대한 이해가 없다면 먼저 이해한 뒤 보길 바란다.
아래 두 포스팅을 참고해도 좋다.
2023.12.02 - [Programming/JAVA] - [Java] AOP 1편 - 핵심 기술 Proxy, Dynamic proxy, Factory bean
[Java] AOP 핵심 기술 - Proxy, Dynamic proxy, Factory bean
목차 AOP란 프락시(Proxy)의 정의, 프락시 패턴 다이내믹 프락시 다이내믹 프락시 예제 한계 스프링의 ProxyFactoryBean : 어드바이저 Spring 3대 기술 (DI, PSA, AOP) 중 하나인 AOP는 프락시 기반으로 동작한
kghworks.tistory.com
2023.12.03 - [Programming/JAVA] - [Spring] AOP 2편 - Spring의 AOP과 @Transactional
[Spring] AOP 2편 - Spring의 AOP과 @Transactional
2023.12.02 - [Programming/JAVA] - [Java] AOP 핵심 기술 - Proxy, Dynamic proxy, Factory bean [Java] AOP 핵심 기술 - Proxy, Dynamic proxy, Factory bean 목차 AOP란 프락시(Proxy)의 정의, 프락시 패턴 다이내믹 프락시 다이내믹 프
kghworks.tistory.com
목차
- 스프링과 JPA의 트랜잭션 동작 추상화
- @Transactional과 EntityManger
- Spring Data JPA SimpleJpaRepository의 @Transactional
스프링과 JPA의 트랜잭션 동작 추상화
스프링의 트랜잭션 동작에 대한 명세는 org.springframework.transaction.TransactionManager에 되어있다.

JPA 를 디비 액세스 기술로 사용하게 되면 org.springframework.orm.jpa.JpaTransactionManager를 사용하게 된다.

즉 Spring의 트랜잭션 동작은 DB 액세스 기술에 따라 구현체가 다르며 JPA를 사용하면 JpaTransactionManager 구현체로 트랜잭션이 동작하게 된다. Hibernate를 사용하면 HibernateTransactionManager를 사용한다. MyBatis 또는 기본적으로는 DataSourceTransactionManger가 사용된다.
@Transactional AOP
@Transactional을 적용한 클래스는 스프링 AOP 다이내믹 프락시로 생성된다.

- BeanFactoryTransactionAttributeSourceAdvisor 생성
- @Transactional 속성 저장 : TransactionAttributeSource
- Advice 지정 : TransactionInterceptor

TransactionInterceptor가 Aspect Advice에 해당한다. @Transactional 프락시에 대한 부가기능은 당연히 트랜잭션 앞뒤 처리에 대한 것일 것이다. TransactionInterceptor은 어디서 오는 걸까?

setTransactionManager()로 트랜잭션 매니저를 생성한 다음 TransactionInterceptor를 생성하는 걸 볼 수 있다. 정리해보자면, @Transactional이 적용된 오브젝트의 프락시는 아래와 같이 생성된다.
- 스프링 Application 구동
- @Transactional이 붙은 class를 탐색
- BeanFactoryTransactionAttributeSourceAdvisor 생성
- @Transactional 에 지정한 속성 값을 TransactionAttributeSource에 저장
- Advice 지정 : TransactionInterceptor (TransactionManager 정보를 가진다, JPA면 JpaTransactionManger)
- BeanFactoryTransactionAttributeSourceAdvisor가 컨테이너에 프락시를 반환
그리고 TransactionInterceptorr가 가진 TransactionManager 구현체는 당연히 디비 액세스 기술에 따라 다르겠지만 JPA를 사용 중이라면 JpaTransactionManager이다.


JPA는 반드시 트랜잭션 안에서 동작하도록 설계되어 있다.

doBegin()은 TransactionManager의 트랜잭션 시작 메서드이다. 그렇다면 구현체 JpaTransacionManager은 어떻게 해당 메서드를 구현했을까. 아래 docs를 보면 doBegin에 대해 정의해 두고 구현해 둔 것을 확인할 수 있다.
Begin a new transaction with semantics according to the given transaction definition.
출처 : docs.spring.io Class JpaTransactionManager


doBegin()에서 EntityManagerFactory로부터 EntityManager를 생성하는 것을 볼 수 있다. 즉 트랜잭션이 시작되는 순간 EntityManger를 생성하는 것이다.
- @Transactional의 프락시는 JpaTransactionManger를 가진 TransactionInterceptor를 Advice로 가진다
- @Transactional 프락시가 호출되면 doBegin()이 실행되는데, JPA의 경우 이때 EntityManger가 생성된다
@PersistenceContext
주제를 벗어난 얘기이지만 아래 테스트 코드에 도움이 될 수 있다고 생각해 덧붙인다. EntityManger를 사용해 JPA 코드를 작성하기 위해 @PersistenceContext을 사용해 의존성을 주입하곤 한다.
@SpringBootTest
@Transactional
public class JpaTest {
@PersistenceContext
private EntityManager entityManager;
....
한 가지 이상한 게 있다. EntityManger는 분명 트랜잭션마다 생성되는 것인데, 어떻게 위처럼 JpaTest 전체에서 하나의 entityManager를 사용할 수 있는 것일까? EntityManger를 하나로 모든 @Test에서 공유해서 사용하는 것일까?
일단 EntityManger는 빈으로 등록되지 않는다. 그리고 싱글톤도 아니다. 사실 @PersistenceContext로 주입된 entityManager는 프락시이다. 마치 하나의 오브젝트를 공유하는 것 같지만 스레드 별로 자신의 콘텍스트에 따라 만들어진 독립적인 오브젝트를 연결해 주는 프락시이다. 그렇기 때문에 위 JpaTest에서 @Test별로 독립적인 트랜잭션 (EntityManger)이 실행될 수 있는 것이다.
@Test
@DisplayName("entityManager is proxy")
public void entityManagerIsProxy() {
assertThat(entityManager.getClass().getName(), not("jakarta.persistence.EntityManager"));
System.out.println(entityManager.getClass().getName());
}
JPA 테스트 코드
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
@SpringBootTest
@Transactional
public class JpaTest {
@PersistenceContext
private EntityManager entityManager;
@Test
@DisplayName("entityManager is proxy")
public void entityManagerIsProxy() {
assertThat(entityManager.getClass().getName(), not("jakarta.persistence.EntityManager"));
System.out.println(entityManager.getClass().getName());
}
@Test
@DisplayName("Create")
public void create() {
Member member = new Member();
member.setName("Karina");
entityManager.persist(member);
entityManager.flush();
entityManager.clear();
Member saved = entityManager.find(Member.class, member.getId());
assertThat(saved.getName(), is("Karina"));
}
public Member setUp() {
Member member = new Member();
member.setName("Karina");
entityManager.persist(member);
entityManager.flush();
entityManager.clear();
return member;
}
@Test
@DisplayName("Retrieve")
@Transactional(readOnly = true)
public void retrieve() {
long generatedId = setUp().getId();
Member generated = entityManager.find(Member.class, generatedId);
assertThat(generated, is(notNullValue()));
assertThat(generated.getId(), is(generatedId));
assertThat(generated.getName(), is("Karina"));
}
@Test
@DisplayName("Update")
public void update() {
long generatedId = setUp().getId();
Member generated = entityManager.find(Member.class, generatedId);
generated.setName("Karina (updated)");
entityManager.persist(generated);
entityManager.flush();
entityManager.clear();
Member updated = entityManager.find(Member.class, generated.getId());
assertThat(updated.getName(), is("Karina (updated)"));
}
@Test
@DisplayName("Delete")
public void delete() {
long generatedId = setUp().getId();
Member generated = entityManager.find(Member.class, generatedId);
entityManager.remove(generated);
entityManager.flush();
entityManager.clear();
Member deleted = entityManager.find(Member.class, generatedId);
assertThat(deleted, is(nullValue()));
}
}
Spring Data JPA SimpleJpaRepository의 @Transactional
Spring Data JPA를 사용하다 보면 JpaRepository를 확장한 인터페이스로 리퍼지터리를 구현한다.
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
}
@Test
@DisplayName("Create Spring Data JPA Repository")
@Transactional(propagation = NOT_SUPPORTED)
public void createJpaRepository() {
Member member = new Member();
member.setName("Karina");
memberRepository.save(member);
Member saved = memberRepository.findById(member.getId()).get();
assertThat(saved.getName(), is("Karina"));
}
위 테스트는 성공한다. 기존의 트랜잭션을 무력화하였는데 어떻게 JPA는 트랜잭션이 없어도 EntityManger를 활용해 Commit 할 수 있었을까.

memberRepository는 프락시다. org.springframework.data.jpa.repository.support.SimpleJpaRepository은 memberRepository의 타깃이다.

SimpleJpaRepository의 save()에 가보면 @Transactional이 선언되어 있는 것을 알 수 있다. 즉 memberRepository의 타깃의 메서드에 @Transactional이 있으므로 정상적으로 EntityManger를 생성한 뒤 save()가 실행됨을 알 수 있다.
참고
JpaTransactionManager (Spring Framework 6.1.1 API)
Return a transaction object for the current transaction state. The returned object will usually be specific to the concrete transaction manager implementation, carrying corresponding transaction state in a modifiable fashion. This object will be passed int
docs.spring.io
https://product.kyobobook.co.kr/detail/S000000935360
토비의 스프링 3.1 세트 | 이일민 - 교보문고
토비의 스프링 3.1 세트 | 애플리케이션 아키텍처 설계부터 프레임워크 제작까지 다룬 스프링 가이드북!『토비의 스프링 3.1 세트』는 스프링을 처음 접하거나 스프링을 경험했지만 스프링이 어
product.kyobobook.co.kr
'Programming > Languages (Java, etc)' 카테고리의 다른 글
[Kotlin] runBlocking vs coroutineScope (0) | 2024.08.02 |
---|---|
[Java] JDK 21 Virtual Threads 톺아보기 (1) | 2024.01.04 |
[Spring] AOP 2편 - Spring의 AOP과 @Transactional (1) | 2023.12.03 |
[Java] AOP 1편 - 핵심 기술 Proxy, Dynamic proxy, Factory bean (1) | 2023.12.02 |
[JPA] Spring Boot JPA 초간단 CRUD 테스트 작성하기 (1) | 2023.11.29 |
댓글