본문 바로가기

SearchDeveloper/SpringBoot

[JPA] Multi Datasource 설정하기

Spring Data Jpa 에서 한 개의 DB만 사용한다면 application.yml 만 설정해도 auto configuration 을 통해 자동으로 DB 커넥션이 생성된다. 하지만 한 프로젝트에 접근할 DB가 2개 이상이라면? 혹은 다른 DB 서버도 접근해야한다면? 귀찮지만 Configuration 클래스를 생성 해줘야한다.


※ 참고

single datasource 일때 application.yml 설정을 참고하면 좋을 듯하다.

application.yml

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/member
    username: sa
    password:
    hikari:
      idle-timeout: 0
      maximum-pool-size: 1
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    properties:
      hibernate.show_sql: true
      hibernate.format_sql: true
      hibernate.use_sql_comments: true
      hibernate.use_query_cache: false

1. application.yml 설정하기

application.yml 에서는 다음과 같이 datasource 설정만 한다.

spring:
  member-datasource:
    jdbc-url: jdbc:h2:tcp://localhost/~/member
    username: sa
    password:
    idle-timeout: 0
    maximum-pool-size: 1
  book-datasource:
    jdbc-url: jdbc:h2:tcp://localhost/~/book
    username: sa
    password:
    idle-timeout: 0
    maximum-pool-size: 1

spring.member-datasource 까지는 변수명을 작성하고 싶은대로 하면 된다. 하지만 그 아래 jdbc-url 같은 속성들은 위 변수명과 똑같이 쓰는 것을 추천한다. 아래에서 확인할 Configuration 클래스에서 DataSource 를 매핑하기가 훨씬 편하기 때문이다.

2. Configuration 클래스 생성하기

일단, 각 datasource 에서 사용할 entity 패키지와 repository package를 분리해줘야한다. 각 Configuration 클래스 마다 패키지 경로를 명시해야하기 때문이다. 필자는 다음과 같이 패키지 구성을 하였다.

전체 코드

MemberDatasource.java

@Configuration
@EnableJpaRepositories(
        basePackages = "dev.hyein.jpamultidatasource.repository.member"
        , entityManagerFactoryRef = MemberDatasource.ENTITY_MANAGER_FACTORY
        , transactionManagerRef = MemberDatasource.TRANSACTION_MANAGER
)
public class MemberDatasource {
    public static final String DATA_SOURCE_NAME = "member";
    public static final String DATA_SOURCE = DATA_SOURCE_NAME + BeanName.DATA_SOURCE; // memberDataSource
    public static final String TRANSACTION_MANAGER = DATA_SOURCE_NAME + BeanName.TRANSACTION_MANAGER; // memberTransactionManager
    public static final String ENTITY_MANAGER_FACTORY = DATA_SOURCE_NAME + BeanName.ENTITY_MANAGER_FACTORY; // memberEntityManagerFactory

    @Bean(ENTITY_MANAGER_FACTORY) 
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(datasource());
        em.setPackagesToScan("dev.hyein.jpamultidatasource.entity.member");
        em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        em.setJpaPropertyMap(
                Map.of(
                        "hibernate.show_sql", "true"
                        , "hibernate.format_sql", "true"
                        , "hibernate.use_sql_comments", "true"
                        , "hibernate.physical_naming_strategy", "org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy"
                        , "hibernate.use_query_cache", "false"
                )
        );
        return em;
    }

    @Bean(DATA_SOURCE)
    @ConfigurationProperties(prefix = "spring.member-datasource")
    public DataSource datasource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(TRANSACTION_MANAGER)
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
        jpaTransactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
        return jpaTransactionManager;
    }
}

핵심은 @EnableJpaRepositories 이다.

@EnableJpaRepositories(
        basePackages = "dev.hyein.jpamultidatasource.repository.member"
        , entityManagerFactoryRef = MemberDatasource.ENTITY_MANAGER_FACTORY
        , transactionManagerRef = MemberDatasource.TRANSACTION_MANAGER
)

몇 가지 설정을 통해 각 datasource를 JPA Repository 로 접근할 수 있게 해준다.

  • basePackages : Repository 클래스가 위치한 패키지 경로
  • entityManagerFactoryRef : EntityManagerFactory Bean
  • transactionManagerRef : TransactionManager Bean

이제 구성물을 하나하나 파헤쳐보자!

① @Bean(ENTITY_MANAGER_FACTORY)

EntityManagerFactory 를 생성한다.

public static final String DATA_SOURCE_NAME = "member";
public static final String ENTITY_MANAGER_FACTORY = DATA_SOURCE_NAME + BeanName.ENTITY_MANAGER_FACTORY; // memberEntityManagerFactory

@Bean(ENTITY_MANAGER_FACTORY)       // [1]
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();   // [2]
    em.setDataSource(datasource());       // [3]
    em.setPackagesToScan("dev.hyein.jpamultidatasource.entity.member"); // [4]
    em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());    // [5]
    em.setJpaPropertyMap(    // [6]
            Map.of(
                    "hibernate.show_sql", "true"
                    , "hibernate.format_sql", "true"
                    , "hibernate.use_sql_comments", "true"
                    , "hibernate.physical_naming_strategy", "org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy"
                    , "hibernate.use_query_cache", "false"
            )
    );
    return em;
}
  • [1] : Bean name 을 @EnableJpaRepositories 에서도 사용하기 때문에 재사용 가능한 상수로 선언하였다. 다른 데이터소스와 중복되지 않도록 반드시 지정해주는 것이 좋다.
  • [2] : LocalContainerEntityManagerFactoryBean 를 생성한다. 이 객체에다가 datasource가 하나였을 때 application.yml 에서 했던 다양한 설정들을 적용해 줄 것이다.
  • [3] : spring.datasource 와 매핑되는 영역이다. 아래 ② @Bean(DATA_SOURCE) 에서 자세히 알아보자
  • [4] : @Entity 클래스가 위치할 패키지 경로를 선언한다.
  • [5] : spring.jpa 와 매핑되는 영역이다. 별도 설정이 없는 경우 HibernateJpaVendorAdapter 객체를 바로 생성 후 넣어준다. Dialect의 경우 jdbc-url 을 보고 자동으로 찾아주기 때문에 커스텀 Dialect 가 아닌 이상 굳이 선언해줄 필요 없다.

spring.jpa
HibernateJpaVendorAdapter 에는 이런 설정들이 있다.

[6] : [spring.jpa.properties](http://spring.jpa.properties) 와 매핑되는 영역이다. application.yml에서도 별도 자동완성이 없었듯이 이 부분도 Map 을 통해 key, value 형태로 넣어준다. 이 경우 DB 필드(snake) 와 Entity 필드(camel) 이 자동 매핑이 안돼서 다음을 추가해주었다.

"hibernate.physical_naming_strategy", "org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy"

spring.jpa.properties

② @Bean(DATA_SOURCE)

public static final String DATA_SOURCE_NAME = "member";
public static final String DATA_SOURCE = DATA_SOURCE_NAME + BeanName.DATA_SOURCE; // memberDataSource

@Bean(DATA_SOURCE)
@ConfigurationProperties(prefix = "spring.member-datasource")
public DataSource datasource() {
    return DataSourceBuilder.create().build();
}
  • spring.datasource 와 매핑되는 영역이다.

spring.datasource

  • 내부적으로 HikariDataSource 객체를 사용하기 때문에 기존 application.yml 의 hikari connection pool 설정도 가능하다.
  • application.yml 값을 @ConfigurationProperties 를 통해 그대로 DataSource 객체와 매핑한다. 부모 클래스인 HikariConfig 에서 여러 설정값들을 인스턴스 변수로 받기 때문에 가능한 일이다.
  • 단, application.yml 과 클래스가 자동 매핑될 수 있게 yml의 변수명을 인스턴스 변수에 맞춰 생성해야한다. 그게 아닌 yml 변수명을 마음대로 작성하면 매핑이 안돼 null 이 떨어질 것이다.
  • 어떤 설정값이 존재하는지는 디버깅으로 DataSource 변수를 확인하거나 HikariConfig.java 에서 확인할 수 있다.

설정값 확인

  • 예외: 변수가 maxPoolSize 라고해서 application.yml 에 max-pool-size: 1 이라고 선언하면 매핑이 안 된다. 왜냐면 내부적으로는 maxPoolSize 를 MaximumPoolSize로 접근하기 때문에 maximum-pool-size: 1 로 선언해줘야한다!

HikariConfig.java

③ @Bean(TRANSACTION_MANAGER)

트랜잭션 관리를 위한 빈이다.

public static final String DATA_SOURCE_NAME = "member";
public static final String TRANSACTION_MANAGER = DATA_SOURCE_NAME + BeanName.TRANSACTION_MANAGER; // memberTransactionManager

@Bean(TRANSACTION_MANAGER)
public PlatformTransactionManager transactionManager() {
    JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
    jpaTransactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
    return jpaTransactionManager;
}
  • PlatformTransactionManager 를 생성 후 위에서 생성한 EntityManagerFactory 를 설정해 반환한다.

3. 쿼리 호출하기

이제 multi datasource 설정은 끝났다! entity 와 repository 등 나머지는 기존 방식 그대로 해주면 된다.

Student Entity

package dev.hyein.jpamultidatasource.entity.member;

import lombok.*;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "student")
@AllArgsConstructor @NoArgsConstructor @Getter @Setter @ToString
public class Student {
    @Id
    private Integer id;
    private String name;
    private String school;
}

 

Student Repository

package dev.hyein.jpamultidatasource.repository.member;

import dev.hyein.jpamultidatasource.entity.member.Student;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudentRepository extends JpaRepository<Student , Integer> {
}

 

StudentRepositoryTest

@SpringBootTest
class StudentRepositoryTest {
    @Autowired
    StudentRepository studentRepository;

    @Test
    @DisplayName("student db 연동 테스트")
    void count() {
        Assertions.assertNotEquals(0, studentRepository.count());
    }
}

 

ScienceRepositoryTest

@SpringBootTest
class ScienceRepositoryTest {
    @Autowired
    ScienceRepository scienceRepository;

    @Test
    @DisplayName("book db 연동 테스트")
    void count() {
        Assertions.assertNotEquals(0, scienceRepository.count());
    }
}

test 결과

전체 코드는 다음에서 확인할 수 있다.

https://github.com/AwesomeHye/jpa-multidatasource

 

GitHub - AwesomeHye/jpa-multidatasource: (블로그) spring data jpa multi datasource 적용

(블로그) spring data jpa multi datasource 적용 . Contribute to AwesomeHye/jpa-multidatasource development by creating an account on GitHub.

github.com

 

레퍼런스

[JPA] Multi datasource 설정하기

 

글 읽어주셔서 언제나 감사합니다. 좋은 피드백, 개선 피드백 너무나도 환영합니다.