본문 바로가기
Programming/Languages (Java, etc)

[JPA] JPA와 @Transactional

by kghworks 2023. 12. 3.

 @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에 되어있다. 

org.springframework.transaction. TransactionManager

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

TransactionManager 계층

 즉 Spring의 트랜잭션 동작은 DB 액세스 기술에 따라 구현체가 다르며 JPA를 사용하면 JpaTransactionManager 구현체로 트랜잭션이 동작하게 된다. Hibernate를 사용하면 HibernateTransactionManager를 사용한다. MyBatis 또는 기본적으로는 DataSourceTransactionManger가 사용된다.

 

@Transactional AOP 

@Transactional을 적용한 클래스는 스프링 AOP 다이내믹 프락시로 생성된다.

org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration

  1. BeanFactoryTransactionAttributeSourceAdvisor 생성
  2. @Transactional 속성 저장 : TransactionAttributeSource
  3. Advice 지정 : TransactionInterceptor

@Transactional이 적용된 클래스의 프락시 생성

 

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

ProxyTransactionManagementConfiguration의 transactionInterceptor()

 

 setTransactionManager()로 트랜잭션 매니저를 생성한 다음 TransactionInterceptor를 생성하는 걸 볼 수 있다. 정리해보자면, @Transactional이 적용된 오브젝트의 프락시는 아래와 같이 생성된다.

  1. 스프링 Application 구동
  2. @Transactional이 붙은 class를 탐색
  3. BeanFactoryTransactionAttributeSourceAdvisor 생성
  4. @Transactional 에 지정한 속성 값을 TransactionAttributeSource에 저장
  5. Advice 지정 : TransactionInterceptor (TransactionManager 정보를 가진다, JPA면 JpaTransactionManger)
  6. BeanFactoryTransactionAttributeSourceAdvisor가 컨테이너에 프락시를 반환

 그리고 TransactionInterceptorr가 가진 TransactionManager 구현체는 당연히 디비 액세스 기술에 따라 다르겠지만 JPA를 사용 중이라면 JpaTransactionManager이다. 

빈 userService의 프락시 탐색
transactionManger : JpaTransactionManager

 

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

AbstractPlatformTransactionManager의 doBegin()

 doBegin()은 TransactionManager의 트랜잭션 시작 메서드이다. 그렇다면 구현체 JpaTransacionManager은 어떻게 해당 메서드를 구현했을까. 아래 docs를 보면 doBegin에 대해 정의해 두고 구현해 둔 것을 확인할 수 있다.

Begin a new transaction with semantics according to the given transaction definition.

출처 : docs.spring.io Class JpaTransactionManager

JpaTransactionManager의 doBegin()
JpaTransactionManager의 createEntityManagerForTransaction()

 

 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 프락시의 타겟은 SimpleJpaRepository

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

SimpleJpaRepository의 save()

 SimpleJpaRepository의 save()에 가보면 @Transactional이 선언되어 있는 것을 알 수 있다. 즉 memberRepository의 타깃의 메서드에 @Transactional이 있으므로 정상적으로 EntityManger를 생성한 뒤 save()가 실행됨을 알 수 있다.


참고

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/orm/jpa/JpaTransactionManager.html

 

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

 

댓글