[JPA/Querydsl] Multiple Databases(다중 DB) 설정하기

Kai·2023년 2월 13일
1

JPA

목록 보기
4/5

☕ 시작


이번 글에서는 하나의 Spring 서비스에서 2개 이상의 DB를 사용할 때 설정하는 방법에 대해서 알아보도록 하겠다.

[버전 정보]
Spring boot: 3.0.2
Spring Data JPA: 3.0.1
QueryDsl: 5.0.0


🌠 무엇을 해야할까?


먼저, 하나의 DB를 사용할 때는 application.properties 또는 application.yml에 DB에 대한 몇가지 설정만 해주면, 알아서 Entity와 Repository들을 스캔해서 바로 개발을 할 수 있게 세팅을 해준다.

하지만, 다중 DB를 설정하기 위해서는 JPA에서 자동으로 설정해주었던 것들을 직접 설정해주어야한다.
크게 보면 아래의 2가지 설정을 해주면 된다.
하나 하나 알아보도록 하자!

  1. Datasource 설정
  2. EntityManager와 QueryFactory Bean등록

QueryFactory Bean을 등록하는 작업은 QueryDsl을 사용하는 사람만 해주면 된다.


1. application.properties


spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# 첫번째 DB에 대한 설정
spring.datasource.url=jdbc:mysql://localhost:3306/{DB명}
spring.datasource.username={DB 유저}
spring.datasource.password={DB 패스워드}

# 두번째 DB에 대한 설정
spring.second-datasource.url=jdbc:mysql://localhost:3306/{또 다른 DB명}
spring.second-datasource.username={DB 유저}
spring.second-datasource.password={DB 패스워드}

기본 설정은 이렇게 해주면 된다.
여기서 second-datasource는 자기가 원하는대로 이름을 붙이면 된다.
예를 들면,second.datsource 라고 해도 전혀 상관없다.


2. Datasource


2개의 Datasource를 만든다고 가정하고, 첫번째 Datasource는 PrimaryDatasource, 두번째 Datasource는 SecondDatasource라고 부르도록 하겠다.

바로 설정과 관련된 코드를 살펴보자.
설명이 필요한 부분은 주석으로 설명하도록 하겠다.

2-1) PrimaryDatasource

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        basePackages = "study.demo.repositories.primary", // 첫번째 DB가 있는 패키지(폴더) 경로
        entityManagerFactoryRef = "primaryEntityManagerFactory", // EntityManager의 이름
        transactionManagerRef = "primaryTransactionManager" // 트랜잭션 매니저의 이름
)
public class PrimaryDatasourceConfig {

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource") // application.properties에 작성된 DB와 관련된 설정 값들의 접두사
    public DataSourceProperties primaryDatasourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.configuration") // DB와 관련된 설정값들의 접두사에 .configuration을 붙여준다.
    public DataSource primaryDatasource() {
        return primaryDatasourceProperties()
                .initializeDataSourceBuilder()
                .type(HikariDataSource.class)
                .build();
    }

    @Bean(name = "primaryEntityManagerFactory")
    @Primary
    public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        DataSource dataSource = primaryDatasource();
        return builder
                .dataSource(dataSource)
                .packages("study.demo.models.primary") // 첫번째 DB와 관련된 엔티티들이 있는 패키지(폴더) 경로
                .persistenceUnit("primaryEntityManager")
                .build();
    }

    @Bean(name = "primaryTransactionManager")
    @Primary
    public PlatformTransactionManager primaryTransactionManager(
            final @Qualifier("primaryEntityManagerFactory") LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean
    ) {
        return new JpaTransactionManager(localContainerEntityManagerFactoryBean.getObject());
    }

}

이렇게 4개의 Bean을 만들어주면 된다.

여기서 주의할 점은 첫번째 DB 설정 클래스 안에 있는 Bean들에 @Primary를 붙여줘야한다는 것이다.


2-2) SecondDatasource

위와 같은 방식으로 두번째 DB와 관련된 설정 클래스를 하나 더 만들어주면 된다.
아까 만든 Datasource와 동일한 내용이므로 상세한 설명은 생략하도록 하겠다.

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        basePackages = "study.demo.repositories.second",
        entityManagerFactoryRef = "secondEntityManagerFactory",
        transactionManagerRef = "secondTransactionManager"
)
public class SecondDataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.second-datasource")
    public DataSourceProperties secondDatasourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @ConfigurationProperties("spring.second-datasource.configuration")
    public DataSource secondDatasource() {
        return secondDatasourceProperties()
                .initializeDataSourceBuilder()
                .type(HikariDataSource.class)
                .build();
    }

    @Bean(name = "secondEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean secondEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        DataSource dataSource = secondDatasource();
        return builder
                .dataSource(dataSource)
                .packages("study.demo.models.second")
                .persistenceUnit("secondEntityManager")
                .build();
    }

    @Bean(name = "secondTransactionManager")
    public PlatformTransactionManager secondTransactionManager(
            final @Qualifier("secondEntityManagerFactory") LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean
    ) {
        return new JpaTransactionManager(localContainerEntityManagerFactoryBean.getObject());
    }

}

☕ 중간점검


이렇게 하면, 다중 DB설정을 위한 재료들이 준비된 것이라고 할 수 있다.
이제 필요한 Bean들을 등록해주자.


3. Bean등록


import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class AppConfig {

	/* JPA 관련 설정 */
    @PersistenceContext(unitName = "primaryEntityManager")
    private EntityManager primaryEntityManager;

    @PersistenceContext(unitName = "secondEntityManager")
    private EntityManager secondEntityManager;

	/* QueryDsl 관련 설정 */
    @Primary // ⭐
    @Bean
    public JPAQueryFactory primaryQueryFactory() {
        return new JPAQueryFactory(primaryEntityManager);
    }

    @Bean
    @Qualifier("SecondQueryFactory")
    public JPAQueryFactory rawDataQueryFactory() {
        return new JPAQueryFactory(secondEntityManager);
    }

}

이렇게 EntityManager와 JPAQueryFactory를 2개씩 등록해주고, 이 Bean들을 필요한 곳에서 주입해서 사용하면 된다.


4. 추가 설정


4-1) Naming strategy ⭐

원래 JPA의 기본 설정상으로는 DB의 테이블이나 필드 이름이 Snake_case로 작성되어 있어도, camelCase로 코드상에서는 사용할 수 있게 해준다.
근데 다중 DB설정을하면 이 부분이 동작을 안한다...

그래서 나는 Entity들에 테이블과 필드명을 모두 다시 설정해주었다.
글로벌하게 설정하는 방법을 찾지 못했다 ㅎㅎ;;
혹시 알고 있는 분이 있으면 댓글로 알려주면 감사하겠다. 🙏

4-2) Data JPA를 사용하시나요?

Data JPA를 사용한다면, 위의 과정대로 설정했다면, 바로 사용할 수 있다.

4-3) QueryDsl을 사용하시나요?

QueryDsl을 사용한다면, 아까 생성한 2개의 JPAQueryFactory 빈을 필요한 상황에 맞게 주입해서 사용하면 된다.
단, JPAQueryFactory 클래스로 등록한 Bean이 2개 이므로 Primary Bean을 사용하는 게 아니라면, 아래와 같이 @Qualifier 어노테이션을 사용해서 원하는 Bean을 직접 주입해서 사용하도록 하자.

@Autowired
public void setQueryFactory(@Qualifier("SecondQueryFactory") JPAQueryFactory queryFactory) {
	this.queryFactory = queryFactory;
}

☕ 마무리


실무를 하다가 이렇게 2개의 DB를 설정해야할 일이 생겨서 이렇게 글을 쓰게 됐는데, 여러 블로그나 문서들이 조금씩 이야기하는 게 조금씩 다르고, 최신 버전의 Spring boot, JPA에 대해 딱 맞는 설정이 없어서 이렇게 글을 쓰게 되었다 ㅎㅎ

이 글이 누군가에게 도움이 되길 바라며 마치도록 하겠다 🙏


참고


2개의 댓글

comment-user-thumbnail
2023년 3월 6일

4-1) Naming strategy가 제대로 동작하지 않는 이유는 properties를 설정해주지 않아서 입니다.
아래 코드와같이 application.property에서 작성한 jpa, hibernate property값을 가져와 넣어주면 정상동작할겁니다.

@RequiredArgsConstructor
...
public class SecondDataSourceConfig {
    private final JpaProperties jpaProperties;

    private final HibernateProperties hibernateProperties;

    @Bean(name = "primaryEntityManagerFactory")
    @Primary
    public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(EntityManagerFactoryBuilder builder) {
Map<String, Object> properties = hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());
        DataSource dataSource = primaryDatasource();
        return builder
                .dataSource(dataSource)
                .packages("study.demo.models.primary") // 첫번째 DB와 관련된 엔티티들이 있는 패키지(폴더) 경로
                .persistenceUnit("primaryEntityManager")
                .properties(properties)
                .build();
    }
    ...
}
1개의 답글