[Spring DB] JPA 트랜잭션 매니저 커스텀하기

개요

 JPA는 내부에 하나의 EntityManagerFactory가 존재하고, EntityManagerFactory가 여러개의 EntityManager들을 생성할 수 있으며, 이렇게 생성된 EntityManager는 영속성 컨텍스트를 생성해 엔티티의 영속성을 관리한다. EntityManager는 트랜잭션 범위 내에서 작동하는 것이 일반적이기 때문에 트랜잭션을 시작할 때 마다 새로운 EntityManger 인스턴스가 필요하며, 이는 트랜잭션 종료 시 폐기된다.

스프링 컨테이너에 등록된 트랜잭션 매니저는 데이터베이스 커넥션을 획득하고, 반납하는 역할을 한다. 스프링에는 데이터베이스 커넥션 풀이라는 것이 존재하는데, 스프링은 빈으로 등록된 Datasource 객체를 통해 커넥션 풀을 관리한다. 이 커넥션 풀에 있는 커넥션들을 트랜잭션 매니저가 필요시 획득하여 사용 후 반납할 수 있다.

 

위 내용들은 예전에 JPA에 관한 공부를 하면서 알게 된 내용들이다.

 

그런데 스프링이 관리하는 트랜잭션 매니저를 우리가 커스텀할 수는 없을까? 스프링은 프로퍼티나 YAML파일을 통해서 datasource에 관한 다양한 설정을 할 수 있도록 지원한다. 하지만 오늘은 개발자가 직접 트랜잭션 매니저를 커스텀하는 방법에 대해서 알아보도록 하겠다.

 

 

트랜잭션 매니저 커스텀하기

 

이전에 트랜잭션 매니저에 관련된 포스팅을 했을때, PlatformTransactionManager는 AbstractPlatformTransactionManager를 구현하는 다양한 구현체들을 가지며, 스프링은 사용하는 DB 액세스 기술에 따라서 적절한 구현체를 사용한다고 공부했던 적이 있다. 

 

어떤 프로젝트에서 MyBatis와 JPA를 동시에 사용할 필요가 있다고 하자. JPA는 ORM 기술이고, MyBatis는 작성한 SQL쿼리를 자바 객체에 매핑해주는 역할을 하기 때문에 각각 별개로 사용할 수 있지만, 동일 데이터베이스에 접근하게 될 경우 JDBC 트랜잭션을 사용하는 MyBatis와 JPA 트랜잭션 매니저를 사용하는 JPA 사이에 문제가 발생할 수 있다.

 

고려해야 할 점은 크게 두 가지가 있을 것이다. 사용하는 트랜잭션 매니저의 설정을 통일해주어야 하고, 동일 Datasource를 사용해야 한다는 점이다. 이를 위해서 우리는 스프링에서 JPA와 MyBatis를 사용하기 위해 설정하는 빈들을 커스텀해줄 것이다.

 

 

1. 동일 Datasource 설정

@Primary
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public DataSource dataSource() {
    return DataSourceBuilder.create().build();
}

 

application.yml의 프로퍼티를 통해서 Datasource를 설정해주고, @Primary 어노테이션을 통해 기존에 스프링에서 사용되던 빈보다 먼저 동작하도록 설정할 수 있다. 이렇게 설정한 Datasource는 JPA와 MyBatis 설정시 사용된다.

 

 

2. JPA 설정

@Primary
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
        EntityManagerFactoryBuilder builder,
        @Qualifier("dataSource") DataSource dataSource,
        Environment env) {
    Map<String, Object> properties = new HashMap<String, Object>();

    properties.put("hibernate.hbm2ddl.auto", env.getRequiredProperty("spring.jpa.hibernate.ddl-auto"));
    properties.put("hibernate.dialect", env.getRequiredProperty("spring.jpa.database-platform"));

    return builder.dataSource(dataSource)
            .packages("com.eckrin.test") // 엔티티 클래스 경로 지정
            .properties(properties) // env(YAML)에서 가져온 맵 형태의 프로퍼티 설정
            .build();
}

 

스프링은 JPA 설정을 위해 EntityManagerFactory를 구성할 때 LocalContainerEntityManagerFactoryBean을 사용하는데, @Primary 어노테이션을 통해서 스프링의 기존 LocalContainerEntityManagerFactoryBean보다 우선적으로 사용할 빈으로 설정하고 커스텀할 수 있다.

 

앞서 빈으로 등록해둔 DataSource를 사용하였으며, 엔티티 클래스가 존재하는 경로를 설정하였다. 이제 JPA는 이 패키지 하위 경로를 스캔하여 엔티티 클래스를 찾아 관리하게 된다.

 

 

3. MyBatis 설정

@Primary
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource DataSource, ApplicationContext applicationContext) throws Exception {
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(DataSource);
    // mybatis 설정 파일 세팅
    sqlSessionFactoryBean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis/mybatis-config.xml"));
    // mapper.xml 패키지 주소
    sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mybatis/mapper/**/*.xml"));
    return sqlSessionFactoryBean.getObject();
}

@Primary
@Bean
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
}

 

다음으로는 MyBatis에서 사용하는 SqlSessionFactory을 설정해주자. 하지만 SqlSession을 사용하는 일반적인 SqlSessionFactory와 다르게 SqlSessionTemplate을 사용하여 SqlSession이 스프링 트랜잭션 내에서 사용될 수 있도록 보장한다. 

 

무슨 말이냐면, MyBatis는 JDBC를 기반으로 동작하며, xml로 이루어진 SQL 매핑 파일을 JDBC를 통해서 데이터베이스에 전송한다. 이때 SqlSession을 통해서 트랜잭션을 관리하는데, 여기서는 SqlSessionTemplate을 사용해서 스프링 트랜잭션에 위임하기 때문에 아래와 같이 JpaTransactionManager를 사용하면 MyBatis와 JDBC 커넥션을 모두 관리할 수 있게 된다.

 

 

4. TransactionManager 설정

@Primary
@Bean
public JpaTransactionManager transactionManager(@Qualifier("entityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
    // JPA transactional
    JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
    jpaTransactionManager.setEntityManagerFactory(entityManagerFactory.getObject());

    return jpaTransactionManager;
}

 

2번에서 생성한 EntityManagerFactoryBean을 사용해서 JpaTransactionManager를 생성하고, 설정해둔 EntityMangerFactoryBean을 등록해준다.