Mybatis와 Jpa 사용시 트랜잭션 묶어서 사용하는 방법(※ 멀티 datasource 설정 / QueryDsl)

JPA와 Mybatis를 동시에 사용하고 있다.

두 기술의 트랜잭션을 서로 공유하면서 사용하고 있는데 세팅한 방법을 기술한다.

 

실무에선 2개 이상의 데이터베이스를 한프로젝트에서 사용한다. 

application.yml에서는 아래와 같이 세팅한다

spring:
  ${보통 DB이름}: //자유롭게 입력
    datasource:
      type: com.zaxxer.hikari.HikariDataSource
      driverClassName: com.mysql.cj.jdbc.Driver
      jdbcUrl:  ${DB URL}
      username: ${DB 계정이름}
      password: ${DB 계정비밀번호}
      hikari:
        poolName: ${poolName} //자유롭게 입력
        connectionTimeout: ??
        maximumPoolSize: ?
        minimumIdle: ?        
        
  ${보통 DB이름}: //자유롭게 입력
    datasource:
      type: com.zaxxer.hikari.HikariDataSource
      driverClassName: com.mysql.cj.jdbc.Driver
      jdbcUrl:  ${DB URL}
      username: ${DB 계정이름}
      password: ${DB 계정비밀번호}
      hikari:
        poolName: ${poolName} //자유롭게 입력
        connectionTimeout: ??
        maximumPoolSize: ?
        minimumIdle: ?
  • 멀티 database를 세팅할때 핵심은 url이 아닌 jdbcUrl 혹은 jdbc-url을 사용해야 한다.
  • spring boot2.0부터 기본 커넥션 풀이 tomcat-jdbc => HikariCP로 변경되었는데 HikariCP의 Database URL 설정은 jdbcUrl로 사용하기 때문이다.
  • 단일 DB설정의 경우에는 자동으로 url을 jdbcUrl로 인식하여 주입해주므로 url을 사용해도 된다.
  • hikari의 상세설정은 서비스 환경마다 다르기 때문에 알맞게 설정하면된다.

 

이제 application.yml에서 설정한 DB에 접근하기 위한 datasource 설정파일을 만들자

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
/**
 * @EnableTransactionManagement : XML의 <tx:annotation-driven/>와 동일, Spring의 선언적 트랜잭션 처리 기능 활성화
 * 해당 어노테이션을 사용하면 DataSourceTransactionManager로 구성되기 때문에 @Scheduled가 동작하지 않는 이슈가
 * 발생한다. 그래서 트랜잭션 매니저를 JpaTransactionManage로 구현한다
 * @Primary 를 사용해 우선적으로 등록할 트랜잭션 매니져 Bean을 지정
 */
@EnableTransactionManagement
@MapperScan(value="{마이바티스에서 사용할 DAO 경로}", sqlSessionFactoryRef="testSqlSessionFactory")

/**
 * JpaRepository를 상속받아서 사용할 경우 해당 인터페이스가 존재하는 경로를 
 * 명시해줘야 사용가능하다.
 */
@EnableJpaRepositories(
        entityManagerFactoryRef = "testJpaEntityManagerFactory",
        transactionManagerRef = "testTransactionManager",
        basePackages = "{repository 경로}"
)
public class JpaConfig {

    /**
     * Datasource : Connection Pool을 지원하는 인터페이스
     *
     */
    @Primary
    @Bean(name="testDataSource")
    @ConfigurationProperties(prefix="${yml에서 세팅한 DB 경로(택 1)}")
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * SqlSessionFactory : SqlSession을 찍어내는 역할
     * Datasourc를 참조하여 MyBatis와 Mysql 서버를 연동한다. SqlSession을 사용하기 위해 사용한다.
     * @param testDataSource
     * @param applicationContext
     */
    @Primary
    @Bean(name = "testSqlSessionFactory")
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("testDataSource") DataSource testDataSource, ApplicationContext applicationContext) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(testDataSource);
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:${.xml이 세팅된 경로(마이바티스 쿼리가 저장된 곳}"));
        return sqlSessionFactoryBean.getObject();
    }

    /**
     * SqlSessionTemplate : SqlSession을 구현하고 코드에서 SqlSession을 대체하는 역할을 한다. 마이바티스 예외처리나 세션의 생명주기 관리
     * @param testSqlSessionFactory
     */
    @Primary
    @Bean(name="testSqlSessionTemplate")
    public SqlSessionTemplate apiSqlSessionTemplate(SqlSessionFactory testSqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(testSqlSessionFactory);
    }

    /**
     * LocalContainerEntityManagerFactoryBean
     * EntityManager를 생성하는 팩토리
     * SessionFactoryBean과 동일한 역할, Datasource와 mapper를 스캔할 .xml 경로를 지정하듯이
     * datasource와 엔티티가 저장된 폴더 경로를 매핑해주면 된다.
     * @param builder
     * @param dataSource
     * @return
     */
    @Primary
    @Bean( name = "testJpaEntityManagerFactory" )
    public LocalContainerEntityManagerFactoryBean jpaEntityManagerFactory(
            EntityManagerFactoryBuilder builder,
            @Qualifier("testDataSource") DataSource dataSource ) {
        return builder.dataSource(dataSource).packages("${엔티티가 저장된 경로}").build();
    }

    /**
     *  JpaTransactionManager : EntityManagerFactory를 전달받아 JPA에서 트랜잭션을 관리
     */
    @Primary
    @Bean(name = "testTransactionManager")
    public JpaTransactionManager transactionManager(
            @Qualifier("testJpaEntityManagerFactory") LocalContainerEntityManagerFactoryBean mfBean
    ) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory( mfBean.getObject() );
        return transactionManager;
    }
}
  • Spring은 PlatformTransacitonManager 인터페이스로 트랜잭션을 처리한다.
  • 이 인터페이스의 구현체 중 하나인 JpaTransactionManager가 있다. 이는 JPA를 위해 주로 사용하지만 트랜잭션이 사용하고 있는 Datasource에 직접 접근가능하여 일반적인 JDBC를 바로 사용할 수 있다.
  • MyBatis는 트랜잭션 관리를 SqlSession이 아닌 Spring Transaction에 위임한다.
  • 따라서, JpaTransactionManager를 사용하면 JPA와 MyBatis를 같은 트랜잭션으로 묶을 수 있다.
  • 만약 2개 이상의 DB를 설정할 경우 자주 사용하는 DB의 config에 @Primary를 붙여줘야 해당 빈부터 로드하여 충돌이 나지 않는다.

 

@Transactional(value = "testTransactionManager")
  • JpaTransactionManager의 Bean이름을 반드시 value의 값으로 설정해서 사용해야한다.
  • 이것도 망각한채 서비스단에서 에러발생시 혹은 테스트코드에서 rollBack이 되지않아 조금 시간을 날렸다.

 

※ QuerDsl 사용시 유의할점

@Repository
public class Repository extends QuerydslRepositorySupport {

    public Repository() {
        super(Repository.class);
    }

    @Override
    @PersistenceContext(unitName = "testJpaEntityManagerFactory")
    public void setEntityManager(EntityManager entityManager) {
        super.setEntityManager(entityManager);
        this.queryFactory = new JPAQueryFactory(entityManager);
    }
  • 멀티 DB 설정을 했을 경우 어떤 entityManagerFactory를 사용할지 몰라 에러를 낸다.
  • 따라서 어떤걸 사용할지 세팅을 해줘야 한다.

혹은 아래와 같이 사용할 수 있다.

 

@Configuration
public class QueryDslConfig {

    @PersistenceContext(unitName = "testJpaEntityManagerFactory")
    private EntityManager testEntityManager;

    @Bean
    public JPAQueryFactory testJpaQueryFactory() {
        return new JPAQueryFactory(testEntityManager);
    }

}

위의 config에서 설정한 EntityManagerFactory 빈을 영속성 컨텍스트의 unitName으로 주입된 EntityManage를 만들어준다. 그리고 해당 entityManager를 주입받은 JPAQueryFactory 객체를 빈으로 등로한다.

 

@Repository
public class TestQueryRepository {

    private final JPAQueryFactory queryFactory;

    public TestQueryRepository(@Qualifier("testJpaQueryFactory") JPAQueryFactory queryFactory) {
        this.queryFactory = queryFactory;
    }
    
    //...
    
}

QueryDsl를 사용하는 레파지토리에서 해당  jpaQueryFactory 빈을 주입받아 사용하면된다.

그러면 QuerydslRepositorySupport 를 상속받지 않고도 사용할 수 있다.

 

 

참고한 글들을 보면 EntityManagerFactory에 persistence 관련 설정을 했다.

    @Bean
    public EntityManagerFactory entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setDataSource(dataSource());
        entityManagerFactoryBean.setPersistenceUnitName("hello");
        entityManagerFactoryBean.setPersistenceXmlLocation("classpath:/META-INF/persistence.xml");
        entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        entityManagerFactoryBean.afterPropertiesSet();
        return entityManagerFactoryBean.getObject();
    }

 

8.11. Use a Traditional persistence.xml File
Spring Boot will not search for or use a META-INF/persistence.xml by default. If you prefer to use a traditional persistence.xml, you need to define your own @Bean of type LocalEntityManagerFactoryBean (with an ID of ‘entityManagerFactory’) and set the persistence unit name there.
See JpaBaseConfiguration for the default settings.

 

 

참고

(★)1. https://thecodinglog.github.io/jpa/mybatis/spring/2019/09/11/jpa-with-mybatis-in-transaction.html

 

JPA 와 mybatis 를 같은 Transaction 에서 사용하기

JPA 와 MyBatis 를 같은 Transaction 에서 사용하기

thecodinglog.github.io

2.https://www.inflearn.com/questions/12499

 

다른 트랜잭션에 JPA 트랜잭션 참여 - 인프런 | 질문 & 답변

안녕하세요? 영한님 덕분에 JPA를 더 쉽게 이해하게 되었습니다. 감사합니다.   Spring의 PlatformTransactionManager 를 이용해서 직접 가져온 트랜잭션이 있을 때 이 트랜잭션안에서 JPA 를 쓰고 싶은데

www.inflearn.com

(★)3. https://rangerang.tistory.com/70

 

[spring boot] mybatis + jpa multi datasource 설정하기

스프링 부트 프로젝트를 개발하며 2개의 데이터베이스를 연결해야하는 이슈가 생겼다. 기존 프로젝트는 mybatis와 jpa를 섞어서 사용하는 구조이고, multi datasource를 설정하기 위해서는 수동설정이

rangerang.tistory.com

 

4. https://jojoldu.tistory.com/296

 

 

Spring Boot & HikariCP Datasource 연동하기

안녕하세요? 이번 시간엔 Spring Boot & Hikari Datasource 연동하기 예제를 진행해보려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. (공부한 내용을 정리하

jojoldu.tistory.com

 

5. https://bongdev.tistory.com/149

 

@Scheduled 실행 시, Transaction처리

@Scheduled 실행 시, Transaction처리 문제 BatchController로 직접 메소드를 호출하는 작업은 정상적으로 수행되나, @scheduled 에 의해 실행되는 작업에 아래와 같은 에러가 발생한다. 똑같은 메소드를 호출

bongdev.tistory.com