티스토리 뷰
[SpringBoot] 다중 Datasource, JPA+QueryDsl+Mybatis 사용 설정.
종벌🍀 2023. 11. 3. 13:18
지난번에 이기종 DB 를 연결하기 위해
Datasource 를 분리하는 작업을 했었는데
거기에 덧붙여 JPA와 Mybatis를 사용할 수 있게하고 QueryDSL 까지 사용하려고 한다.
[SpringBoot JPA] 다중 DB 설정하기 (multi Datasource + 이기종 DB)
간혹 사이트들을 연계해야 할때, 그 중에서도 api 없는 사이트의 데이터를 사용해야할 때, 하나의 웹에서 여러 DB를 연결 시켜 사용해야 할 때가 있다. 간단하게 DB Link 로 해결하려 했으나 메인으
jong-bae.tistory.com
JPA 설정만으로 분리하는 것은 위 전 포스팅과 같이 매우 간편하게 설정할 수 있었다.
하지만 필자 프로젝트에 Mybatis도 간혹 필요한 부분이 있어서 설정하는데
약간의 예상치 못한 시행착오들이 있어 포스팅 해본다.
프로퍼티 구조는 아래와 같다.
url을 jdbc-url 로 변경하는 것 잊지마시길!
spring:
primary-datasource:
driver-class-name: oracle.jdbc.OracleDriver
jdbc-url : ENC()
username: ENC()
password: ENC()
second-datasource:
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
jdbc-url: ENC()
username: ENC()
password: ENC()
그리고 주DB와 서브DB의 설정클래스를 아래와 같이 작성했다.
@Configuration
@EnableJpaRepositories(
basePackages = {
"com.sample.core.common"
, "com.sample.core.manage"
, "com.sample.core.api"
},
entityManagerFactoryRef = "primaryEntityManager",
transactionManagerRef = "primaryTransactionManager"
)
@MapperScan(
sqlSessionFactoryRef = "primarySessionFactory",
value = {
"com.sample.core.common.mybatis"
, "com.sample.core.manage.**.mapper"
}
)
public class PrimaryConfig {
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.primary-datasource")
public DataSource primaryDatasource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean primaryEntityManager() {
LocalContainerEntityManagerFactoryBean managerFactoryBean = new LocalContainerEntityManagerFactoryBean();
managerFactoryBean.setDataSource(primaryDatasource());
managerFactoryBean.setPersistenceUnitName("primaryPersistence");
managerFactoryBean.setPackagesToScan(new String[] {
"com.sample.core.common"
, "com.sample.core.manage"
, "com.sample.core.api"
});
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(true);
managerFactoryBean.setJpaVendorAdapter(vendorAdapter);
HashMap<String, Object> prop = new HashMap<>();
prop.put("hibernate.hbm2ddl.auto", "validate");
managerFactoryBean.setJpaPropertyMap(prop);
return managerFactoryBean;
}
@Bean
@Primary
public PlatformTransactionManager primaryTransactionManager() {
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
jpaTransactionManager.setEntityManagerFactory(primaryEntityManager().getObject());
return jpaTransactionManager;
}
@Bean(name = "primarySessionFactory")
@Primary
public SqlSessionFactory primarySessionFactory(ApplicationContext applicationContext) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(primaryDatasource());
sqlSessionFactoryBean.setTypeAliasesPackage("com.sample.core");
sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath*:/com/sample/core/**/*Mapper.xml"));
sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource("classpath:mybatis-config.xml"));
return sqlSessionFactoryBean.getObject();
}
}
@Configuration
@EnableJpaRepositories(
basePackages = {"com.sample.core.homepage"},
entityManagerFactoryRef = "secondEntityManager",
transactionManagerRef = "secondTransactionManager"
)
@MapperScan(
sqlSessionFactoryRef = "secondSessionFactory",
value = {"com.sample.core.homepage.**.mapper" }
)
public class SecondConfig {
@Bean
@ConfigurationProperties(prefix = "spring.second-datasource")
public DataSource secondDatasource() {
return DataSourceBuilder.create().build();
}
@Bean
public LocalContainerEntityManagerFactoryBean secondEntityManager() {
LocalContainerEntityManagerFactoryBean managerFactoryBean = new LocalContainerEntityManagerFactoryBean();
managerFactoryBean.setDataSource(secondDatasource());
managerFactoryBean.setPersistenceUnitName("secondPersistence");
managerFactoryBean.setPackagesToScan(new String[] {
"com.sample.core.homepage"
});
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(true);
managerFactoryBean.setJpaVendorAdapter(vendorAdapter);
HashMap<String, Object> prop = new HashMap<>();
prop.put("hibernate.hbm2ddl.auto", "update");
managerFactoryBean.setJpaPropertyMap(prop);
return managerFactoryBean;
}
@Bean
public PlatformTransactionManager secondTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(secondEntityManager().getObject());
return transactionManager;
}
@Bean(name = "secondSessionFactory")
public SqlSessionFactory secondSessionFactory(ApplicationContext applicationContext) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(secondDatasource());
sqlSessionFactoryBean.setTypeAliasesPackage("com.sample.core.homepage");
sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath*:/com/sample/core/homepage/**/*Mapper.xml"));
sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource("classpath:mybatis-config.xml"));
return sqlSessionFactoryBean.getObject();
}
}
기존 다중 Datasource 설정에 mybatis를 위한 mapperscan과 SqlSessionFactory 를 작성해줬다.
그리고 application.yml 에 입력한 mybatis 설정이 설정대로 동작하지 않는 것 같아 mybatis-config.xml 로 작성해서 리소스를 참조하게 변경하였다.
다중 Datasource 를 이용할때는 레파지토리가 있는 패키지 위치를 확실히 구분지어 사용해야한다.
예전 mybatis만 쓸때는 SqlSession bean 이름을 각각 설정해 한 레파지토리에서 사용 가능했지만
JPA 는 구분지어줘야 올바르게 패키지 내의 엔티티를 스캔하여 접근을 한다.
프로젝트 진행 중에 DB를 나누게 되면서 전체구조를 틀기엔 부담이 있어
특정 패키지만 서브DB 를 사용하기로 하고
Primary 쪽에는 패키지명을 다수 나열하게 됐다.
여기까지 패키지가 잘 나눠져 계시고 특이사항이 없다면 잘 구동 되실 겁니다.
그리고 작업중에 전혀 예상치 못했던 부분을 마주하게 됐는데...
Mybatis의 @MapperScan이 @Mapper 라고 명칭된 인터페이스들을 모두 끌어가는 것이었다.
해당 프로젝트에는 Mybatis @Mapper와 Mapstruct @Mapper 인터페이스가 각각 2개씩 존재하는데
Mybatis가 Mapstruct 매퍼까지 인식해서 오류가 발생했다.
import org.mapstruct.Mapper;
import org.apache.ibatis.annotations.Mapper;
아니 두분 전혀 다르신 분인데요...ㅋ
파일이 아래와 같이 구분되어져 있는데...


mybatis의 @MapperScan이 *ToMapper 로 되어 있는 Mapstruct 인터페이스들을 인식하면서
BeanCreationException 이 발생했다.
Initialization of bean failed;
nested exception is org.springframework.beans.ConversionNotSupportedException:
Failed to convert property value of type 'org.mybatis.spring.SqlSessionTemplate' to required type 'org.apache.ibatis.session.SqlSessionFactory' for property 'sqlSessionFactory';
nested exception is java.lang.IllegalStateException:
Cannot convert value of type 'org.mybatis.spring.SqlSessionTemplate' to required type 'org.apache.ibatis.session.SqlSessionFactory' for property 'sqlSessionFactory':
no matching editors or conversion strategy found
동일한 어노테이션 명칭 (@mapper) 이라 모두 스캔에 포함되는 것 같다.
과거에 아래 처럼 mybatis mapper 폴더를 따로 분리하여 처리하였다.
그런데 이후에 특정 패키지의 mybatis mapper interface 를 찾지 못하는 상황이 발생했다.
그래서 MapperScan 에 옵션을 추가하여 조치하였다.
@MapperScan(
sqlSessionFactoryRef = "primarySessionFactory",
annotationClass = org.apache.ibatis.annotations.Mapper.class,
value = {
"com.sample.core.common.mybatis"
, "com.sample.core.manage"
//, "com.sample.core.manage.**.mapper"
}
)
@MapperScan(
sqlSessionFactoryRef = "secondSessionFactory",
annotationClass = org.apache.ibatis.annotations.Mapper.class,
value = {"com.sample.core.homepage"
//"com.sample.core.homepage.**.mapper"
}
)
mybatis는 org.apache.ibatis.annotations.Mapper 만 보도록 설정
그리고 QueryDsl 도 주DB를 바라보도록 아래와 같이 변경하였다.
EntityManagerFactoryBean에 설정한 persistenceUnitName 을 QueryDsl 설정에 명시해준다.
managerFactoryBean.setPersistenceUnitName("primaryPersistence");
managerFactoryBean.setPersistenceUnitName("secondPersistence");
@Configuration
public class QuerydslConfig {
@PersistenceContext(unitName = "primaryPersistence")
private EntityManager entityManager;
@PersistenceContext(unitName = "secondPersistence")
private EntityManager secondEntityManager;
@Primary
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
@Bean
public JPAQueryFactory secondJpaQueryFactory() {
return new JPAQueryFactory(secondEntityManager);
}
}
기존의 Querydsl 설정에 위와 같이 추가해준 후
레파지토리에서 빈이름을 명시해줘 사용하면 된다.
@Repository
@RequiredArgsConstructor
public class queryRepository {
// primary 일경우
private final JPAQueryFactory queryFactory;
// other 일경우
@Autowired
@Qualifier("secondJpaQueryFactory")
private final JPAQueryFactory queryFactory;
...
}
여기까지하면 주DB의 JPA+QueryDsl+Mybatis가 잘 동작하고
서브DB의 JPA+Mybatis 가 잘되는 것을 확인 할 수 있었다.
예상치못한 mybatis 녀석때문에 쫌 많은 시간을 사용한 것 같다.