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
- 실제 동작하는 작업의 단위
- EntityManagerFactory
2. SpringBoot + Spring Data JPA
- 위에서 살펴봤던 순수 JPA 로직 흐름을 기억하고 이번엔 SpringBoot + Spring Data JPA 환경에서 어떻게 EntityManager를 만들고 Transaction을 생성하는지 확인해보겠습니다
- 예제 레포지토리
- 프로젝트 환경 설정
- 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)
- TransactionStatus getTransaction(TransactionDefinition definition)
2-2. JpaTransactionManager
- 위에서 살펴본 PlatformTransactionManager를 구현하는 여러 트랜잭션 매니저 중 오늘 알아볼 주요 대상은 JpaTransactionManager 입니다
- 공식 레퍼런스 정의
- 단일 JPA EntityManagerFactory를 유지하면서 스레드별로 EntityManager를 제공한다고 되어있습니다
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/orm/jpa/JpaTransactionManager.html
- 분석 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이 의도한 대로 생성된 것을 확인할 수 있었다
- method 레벨에 @Transactional 설정
- 분석 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 )
- MyService :: checkTransactional() 호출
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
반응형