Tech/JPA

EntityManager 라이프사이클

주지민 2021. 10. 1. 15:11
반응형
JPA 공부를 하다보니 EntityManager 라이프사이클에 대한 기초지식이 부족해 공부한 내용입니다
SpringBoot + Spring Data JPA 환경에서 Transaction, EntityManager 간의 관계를 주 관심사로 진행했습니다

1. 순수 JPA 로직

  • 먼저 순수 JPA 로직으로 엔티티를 저장하는 순서를 살펴보겠습니다
  • 예제 코드 ( persistence.xml은 생략 )
    // JPA
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("master");
    EntityManager entityManager = emf.createEntityManager();
    EntityTransaction transaction = entityManager.getTransaction();
    
    try {
        transaction.begin();
        ...
        transaction.commit();
    } catch (Exception e) {
        transaction.rollback();
    }
    
    entityManager.close();
    emf.close();

    • EntityManagerFactory
      • 엔티티 매니저를 생성하는 주체
      • Thread-safe하기 때문에 DB당 하나씩만 사용하여 공유합니다
    • EntityManager
      • 엔티티를 관리하는 클래스
      • Thread-safe하지 않기 때문에 상황에 따라 매번 새로운 엔티티매니저를 만들어야한다
      • 엔티티 매니저당 영속성 컨텍스트 하나씩 가짐
      • 엔티티 매니저당 트랜잭션도 하나씩 가짐 ( getTransaction()을 여러번해도 같은 객체를 반환합니다 )
    • EntityTransaction
      • 실제 동작하는 작업의 단위

 

2. SpringBoot + Spring Data JPA

  • 위에서 살펴봤던 순수 JPA 로직 흐름을 기억하고 이번엔 SpringBoot + Spring Data JPA 환경에서 어떻게 EntityManager를 만들고 Transaction을 생성하는지 확인해보겠습니다
  • 예제 레포지토리
 

GitHub - joojimin/datasource-test: Spring Data JPA DataSource, Transaction 테스트 레포

Spring Data JPA DataSource, Transaction 테스트 레포. Contribute to joojimin/datasource-test development by creating an account on GitHub.

github.com

  • 프로젝트 환경 설정
    • SpringBoot 2.5.5
    • Spring Web
    • Spring Data JPA
    • MySQL Connector
    • Lombok
  • application.yml 
    
    # application.yml
    server:
      port: 9090
      output:
        ansi:
          enabled: always
    
    spring:
      datasource:
        jdbc-url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&characterEncoding=UTF-8
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root // 계정
        password: 1234 // 비번
    
      # JPA
      jpa:
        hibernate:
          ddl-auto: none
        properties:
          hibernate:
            format_sql: true
            dialect: org.hibernate.dialect.MySQL8Dialect
        open-in-view: false
    
    logging:
      level:
        org:
          hibernate:
            SQL: debug
            type: trace

    • 환경은 실제 로컬에 띄워져있는 MySQL 환경으로 진행하였습니다
    • 이렇게만 설정하고 부트를 동작시켜도 기본 Default로 설정되어있는 Bean들이 등록되 동작하지만 실제 동작을 더 자세히 이해하기 위해 DataSource Configuration을 생성해봤습니다
    • DataSourceConfig.class
      // DataSourceConfig.class
      @Slf4j
      @RequiredArgsConstructor
      @Configuration
      public class DataSourceConfig {
      
          private final static String DOMAIN_PACKAGE_PATH = "com.example.datasourcetest";
      
          private final JpaProperties jpaProperties;
          private final HibernateProperties hibernateProperties;
      
          @Bean
          @ConfigurationProperties(prefix = "spring.datasource")
          public DataSource dataSource() {
              return DataSourceBuilder.create()
                                      .type(HikariDataSource.class)
                                      .build();
          }
      
          @Bean
          public LocalContainerEntityManagerFactoryBean entityManagerFactory(final EntityManagerFactoryBuilder builder) {
              Map<String, Object> hibernatePropertiesMap = hibernateProperties
                  .determineHibernateProperties(jpaProperties.getProperties(),
                                                new HibernateSettings());
              return builder.dataSource(dataSource())
                            .properties(hibernatePropertiesMap)
                            .packages(DOMAIN_PACKAGE_PATH)
                            .persistenceUnit("master")
                            .build();
          }
      
      
          @Bean
          PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) {
              EntityManagerFactory entityManagerFactory = Objects.requireNonNull(entityManagerFactoryBean(builder).getObject());
              return new JpaTransactionManager(entityManagerFactory);
          }
      }

      • application.yml속 spring.datasource 속성들을 주입받아 DataSource 객체를 생성했습니다 ( 기본 HikariCP )
      • Spring Data JPA는 기본적으로 LocalContainerEntityManagerFactoryBean이라는 EntityManagerFactory를 사용하고 있습니다
      • LocalContainerEntityManagerFactoryBean을 이용하여 JPA의 EntityManager와 Transaction을 관리하는 JpaTransactionManager를 생성 후 빈으로 등록했습니다

 

2-1. PlatformTransactionManager

  • 먼저 생성한 트랜잭션 매니저의 인터페이스를 간략히 정리해보려고 합니다
  • 모든 스프링의 트랜잭션 기능과 코드는 PlatformTransactionManager를 통해서 로우레벨의 트랜잭션 서비스를 이용할 수 있습니다
  • 트랜잭션의 경계를 지정하여, 트랜잭션이 어디서, 어떻게 시작되고 종료하는지 종료시 정상적으로 커밋 혹은 오류로 인한 롤백을 해야하는지 결정하는 주체입니다
  • 해당 인터페이스에는 3가지 기능이 정의되어 있습니다
    • TransactionStatus getTransaction(TransactionDefinition definition)
      • 적절한 트랜잭션을 가져오는 메서드
      • 트랜잭션 속성(PROPAGATION, ISOLATION)에 따라 새로운 트랜잭션을 생성할지, 기존 트랜잭션을 가져와 참여할지 등등의 행동을 하게됩니다
    • void commit(TransactionStatus status)
    • void rollback(TransactionStatus status)

 

2-2. JpaTransactionManager

 

JpaTransactionManager (Spring Framework 5.3.10 API)

Set the name of the persistence unit to manage transactions for. This is an alternative to specifying the EntityManagerFactory by direct reference, resolving it by its persistence unit name instead. If no EntityManagerFactory and no persistence unit name h

docs.spring.io

  • 분석 1. 설정한 JpaTransactionManager를 통해 선언적인(@Transactional) Spring 트랜잭션이 걸리는지 확인
    • method 레벨에 @Transactional 설정
      @Transactional
      public void checkTransactional() {
          System.out.println("hi");
      }​
    • 전역으로 생성되는 JpaTransactionManager 객체 확인
    • Service 객체에 @Transactional 선언 후 Spring Transaction을 생성하는 로직인 getTransactio() 메서드 확인
    • 정리
      • JpaTransactionManager의 부모인 AbstractPlatformTransactionManager를 통해 실제 getTransaction() 메서드 동작을 확인할 수 있다
      • 생성된 JpaTransactionManager@7686 아이디를 통해 Spring Transaction이 의도한 대로 생성된 것을 확인할 수 있었다 

 

  • 분석 2. getTransaction() 메서드 확인
    • Transaction의 기본 설정인 REQURIED(부모 트랜잭션이 있으면 참여, 없으면 생성), ISOLATION_DEFAULT로 진행 
    • EntityManager 생성
      • AbstractPlatformTransactionManager :: getTransaction()
      • AbstractPlatformTransactionManager :: startTransaction()
      • JpaTransactionManager :: doBegin()
      • JpaTransactionManager :: createEntityManagerForTransaction()
      • 위 메서드들을 쭉 따라가다보면 마지막 JpaTransactionManager의 createEntityManagerForTransaction() 메서드를 만나게 되고 여기서 아래와 같은 코드를 확인할 수 있다
        	
        // JpaTransactionManager
        protected EntityManager createEntityManagerForTransaction() {
            EntityManagerFactory emf = obtainEntityManagerFactory();
            Map<String, Object> properties = getJpaPropertyMap();
            EntityManager em;
            if (emf instanceof EntityManagerFactoryInfo) {
                em = ((EntityManagerFactoryInfo) emf).createNativeEntityManager(properties);
        	}
        	else {
        		em = (!CollectionUtils.isEmpty(properties) ?
        				emf.createEntityManager(properties) : emf.createEntityManager());
        	}
        	if (this.entityManagerInitializer != null) {
        		this.entityManagerInitializer.accept(em);
        	}
        	return em;
        }
        • 전역으로 설정한 LocalContainerEntityManagerFactoryBean(EntityManagerFactory)를 통해 실제 EntityManager를 생성하게 된다
    • 생성된 EntityManager를 이용해 AOP로 걸려있는 Spring Transaction Aspect가 getTransaction().begin()을 실제 로직 전에 걸게된다
  • 분석 3. 중첩된 @Transactionl에서 동작
    • @Transactional로 생성된 Service에 다시 @Transactional을 걸어둔 ChildService를 주입받아 중첩으로 실행해봤습니다
    • 마찬가지로 설정은 REQUIRED, ISOLATION_DEFAULT (기본 @Transactional로 진행)으로 진행했습니다
    • 예제 코드
      @RequiredArgsConstructor
      @Service
      public class MyService {
      
          private final ChildMyService childMyService;
      
          @Transactional
          public void checkTransactional() {
              childMyService.test();
          }
      }
      
      
      
      @Service
      public class ChildMyService {
      
          @Transactional
          public void test() {
              System.out.println("hi child service!!");
          }
      }
    • 동작 확인
      • MyService :: checkTransactional() 호출
        • 선언적 트랜잭션(@Transactional)이 선언되어 있기 때문에 Component 등록시 이미 Proxy로 감싸져 있고 위에서 확인했던 대로 의 AbstractPlatformTransactionManager의 getTransaction() 메서드를 호출하게됩니다
        • 이미 기존에 실행된 EntityManager나 Transaction이 없으므로 createEntityManagerForTransaction() 메서드를 호출하게 되고 EntitiyManager(실제론 EntityManager를 감싸고있는 EntityManagerHolder)를 생성하게 된다
        • 여기서 추가적으로 생성된 EntityManager를 ThreadLocal에 Map 형태로 저장을 하게되는데 EntitiyManagerFactory를 키로 생성된 EntityManager를 value로 저장한다
      • ChildMyService :: test() 호출
        • 마찬가지로 트랜잭션이 선언되어 있기 때문에 위와 동일하게 getTransaction() 메서드를 호출하게 됩니다
        • 쭉 따라가다보면 JpaTransactionManager의 doGetTransaction 메서드에서 아래와 같은 코드를 만나게 됩니다
          // JpaTransactionManager :: doGetTransaction
          EntityManagerHolder emHolder = (EntityManagerHolder)
          				TransactionSynchronizationManager.getResource(obtainEntityManagerFactory());
        • 위에서 설명한대로 ThreadLocal에 저장되어있는 값이 존재하면 꺼내오는 로직입니다
        • ChilldMyService::test() 또한 같은 EntityManagerFactory를 사용하기 때문에 MyService에서 생성한 EntityManager를 가져오게 됩니다
        • 즉 같은 EntityManager의 getTransaction()을 호출하기 때문에 ChildMyService::test() 또한 같은 트랜잭션으로 묶이는 것을 확인할 수 있었습니다 ( PROPAGATION_REQUIRED )

 

3. 정리

  • Spring 환경에서 선언적인 트랜잭션(@Transactional)을 이용하게되면 정의된 EntityManagerFactory 한 개만 생성하여 공유한다
  • 정의된 EntityManagerFactory를 이용해 EntityManager를 생성하게되고, 이는 Thread별로 ThreadLocal 자료구조에 정의된 EntityManagerFactory를 Key, 생성된 EntityManager를 Value로 저장되어 관리된다
  • 같은 EntityManager를 통해 getTransaction() 메서드를 호출하면 같은 트랜잭션이 나오게된다 ( PROPAGATION_REQUIRED, default 일 때만 )

 

4. 추가 공부 사항

  • ThreadLocal에 저장되는 엔티티의 기준 키는 EntityManagerFactory이다. 즉 한 개의 어플리케이션에서 두 개 이상의 DataBase를 이용하게 될 경우 각각 EntityManagerFactory를 설정해야하고, 이는 곧 서로 다른 EntityManager, Transaction을 사용한다고 이해가 됐다
  • 다른 PROPAGATION들은 새로운 포스트에 정리하겟습니다

 

728x90
반응형