DEV ℧ Developer Diary

[JPA] JPA 다중 DB 사용 설정

JPA 다중 DB 사용 설정

프로젝트를 진행하던 중 한 프로젝트 내에 DB의 Schema을 두개 연결해야 하는 상황이 생겨 검색해보니, 같은 DB 서버 내에 다른 Schema라고 해도 JPA 내부에서는 다른 DB로 인식하기 때문에 새로이 설정해주어야 한다고 한다.

그럼 두개의 테스트 DB을 만들어서 설정 해보도록 하자.

DB

DB는 H2 Database를 두개 준비했다. 각각 test라는 DB와 jpa라는 DB를 준비했다.

DB1 DB2

각각 test의 서버에는 TEST라는 이름의 Schema, jpa의 서버에는 JPA라는 이름의 Schema를 생성한다.

SCHEMA1 SCHEMA2

application.yml

먼저 기존에 하나만 있는 설정을 두개 H2를 사용하도록 추가한다.

spring:
  datasource:
    jpa:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:tcp://localhost/~/jpa
      username: sa
      password: aaa
    test:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:tcp://localhost/~/test
      username: sa
      password:

여기서 주의할점이 있다면, Spring boot2에서는 기본적으로 HikariCP가 사용되기 때문에 DB의 접속정보를 가져오기 위해서는 url 속성이 아닌 jdbc-url 속성을 사용해서 가져와야한다.

url 속성을 사용한다면 jdbcUrl을 찾을 수 없다는 에러가 발생한다.

Caused by: java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.

Entity

각각 test와 jpa의 schema를 가지는 Entity를 만들어준다. Entity의 schema 구분은 @Table 어노테이션에서 설정 할 수 있다.

@Table(schema = "jpa", name = "JPA_USER")

이렇게 설정하면 jpa 스키마의 JPA_USER 테이블을 가리키게된다.

  • jpa DB의 JpaUser
@Getter
@Entity
@Table(schema = "jpa", name = "JPA_USER")
@NoArgsConstructor
public class JpaUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "JPA_USER_ID")
    private Long id;

    @Column(name = "JPA_USER_NAME")
    private String name;

    @Column(name = "JPA_USER_AGE")
    private Integer age;

    @Column(name = "JPA_USER_STATUS")
    @Enumerated(EnumType.STRING)
    private UserStatus status;
}

UserStatus는 간단하게 enum으로 회원의 상태를 선언했다.

public enum UserStatus {
    ADMIN, USER
}
  • test DB의 TestUser
@Entity
@Getter
@Table(schema = "test", name = "TEST_USER")
@NoArgsConstructor
public class TestUser {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "TEST_USER_ID")
    private Long id;

    @Column(name = "TEST_USER_NAME")
    private String name;

    @Column(name = "TEST_USER_AGE")
    private Integer age;
}

Repository

각각 JpaUser와 TestUser의 Repository를 만들어준다.

public interface JpaUserRepository extends JpaRepository<JpaUser, Long> { }
public interface TestRepository extends JpaRepository<TestUser, Long> { }

이대로 실행한다면, yaml에서 DB 정보를 불러올 수 없다는 에러가 발생한다.

Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Schema "JPA" not found; SQL statement:
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Schema "TEST" not found; SQL statement:

yaml 파일에 DB설정을 해줬으니 각각 엔티티에 적용할 수 있도록 Config파일을 만들어 주어야 한다.

DB CONFIG

  • TEST DB CONFIG
@Configuration
@EnableJpaRepositories(
        entityManagerFactoryRef = "testEntityManagerFactory"
        , transactionManagerRef = "testTransactionManager"
        , basePackages = "com.dhaudgkr.jwtsample.domain.schema.test.repository"
)
public class TestDbConfig {

    private final JpaProperties jpaProperties;
    private final HibernateProperties hibernateProperties;

    public TestDbConfig(JpaProperties jpaProperties, HibernateProperties hibernateProperties) {
        this.jpaProperties = jpaProperties;
        this.hibernateProperties = hibernateProperties;
    }

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

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean testEntityManagerFactory (EntityManagerFactoryBuilder builder) {
        Map<String, Object> properties = hibernateProperties.determineHibernateProperties(
                jpaProperties.getProperties(), new HibernateSettings());

        return builder
            .dataSource(testDataSource())
            .packages("com.dhaudgkr.jwtsample.domain.schema.test.model")
            .persistenceUnit("testEntityManager")
            .properties(properties)
            .build();
    }

    @Bean
    @Primary
    public PlatformTransactionManager testTransactionManager(@Qualifier(value = "testEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }
}
  • JPA DB CONFIG
@Configuration
@EnableJpaRepositories(
        entityManagerFactoryRef = "jpaEntityManagerFactory"
        , transactionManagerRef = "jpaTransactionManager"
        , basePackages = "com.dhaudgkr.jwtsample.domain.schema.jpa.repository"
)
public class JpaDbConfig {

    private final JpaProperties jpaProperties;
    private final HibernateProperties hibernateProperties;

    public JpaDbConfig(JpaProperties jpaProperties, HibernateProperties hibernateProperties) {
        this.jpaProperties = jpaProperties;
        this.hibernateProperties = hibernateProperties;
    }

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

    @Bean
    public LocalContainerEntityManagerFactoryBean jpaEntityManagerFactory (EntityManagerFactoryBuilder builder) {
        Map<String, Object> properties = hibernateProperties.determineHibernateProperties(
                jpaProperties.getProperties(), new HibernateSettings());

        return builder
            .dataSource(jpaDataSource())
            .packages("com.dhaudgkr.jwtsample.domain.schema.jpa.model")
            .persistenceUnit("testEntityManager")
            .properties(properties)
            .build();
    }

    @Bean
    public PlatformTransactionManager jpaTransactionManager(@Qualifier(value = "jpaEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }
}

간단하게 설명을 해보도록 하자.

@EnableJpaRepositories

먼저 처음에 선언된 @EnableJpaRepositories 의경우 JPA Repository의 bean을 활성화 하는 어노테이션이다. 원래 자동으로 실행이 되지만, Repository 별로 DB 설정을 달리 줘야 하므로, 수동으로 적용해 주어야 한다.

  • entityManagerFactoryRef : Repository의 Entity의 package 경로 설정
  • transactionManagerRef : transaction에 대한 설정을 한다.
  • basePackages : 연결한 repository의 package 경로 설정

여기서 주의할점은 basePackages은 repository의 bean을 생성하기 때문에 다른 DB설정이 있을 경우 경로가 겹치면 안된다.

The bean 'jpaUserRepository', defined in com.dhaudgkr.jwtsample.domain.schema.jpa.repository.JpaUserRepository defined in @EnableJpaRepositories declared on TestDbConfig, could not be registered. A bean with that name has already been defined in com.dhaudgkr.jwtsample.domain.schema.jpa.repository.JpaUserRepository defined in @EnableJpaRepositories declared on JpaDbConfig and overriding is disabled.

경로가 겹칠경우 이미 bean 이 생성되었다는 에러가 발생한다.

dataSource 생성

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

@ConfigurationProperties 어노테이션의 prefix 속성으로 yaml파일에서 속성값에 해당하는 DB 연결 주소를 가져와 생성한다.

entityMangerFactor로 Entity 설정

@Bean
public LocalContainerEntityManagerFactoryBean jpaEntityManagerFactory (EntityManagerFactoryBuilder builder) {
    Map<String, Object> properties = hibernateProperties.determineHibernateProperties(
            jpaProperties.getProperties(), new HibernateSettings());

    return builder
        .dataSource(jpaDataSource())
        .packages("com.dhaudgkr.jwtsample.domain.schema.jpa.model")
        .persistenceUnit("testEntityManager")
        .properties(properties)
        .build();
}
Map<String, Object> properties = hibernateProperties.determineHibernateProperties(
            jpaProperties.getProperties(), new HibernateSettings());

해당 부분에서 show_sql, format_sql, use_sql_comments, hbm2ddl.auto 와 같은 hibernate 설정을 가져온다.

hbm2ddl.auto의 옵션의 경우 설정을 하지 않았거나 none일 경우에는 설정을 제거한다.

이후 EntityManagerFactoryBuilder 를 이용해 Entity의 경로 및 DB 접속정보를 설정해준다. Entity의 경로는 Repository와는 다르게 겹쳐도 상관이 없다.

Transaction 설정

EntityManagerFactoryJpaTransactionManager 설정하기에 앞서 위에 Entity 및 DB의 정보를 설정한 EntityManagerFactoryBuilder bean 객체를 불러와야 하기 때문에 @Qualifier을 이용하여 바로 위에 만들어준 bean을 호출해준다.

@Qualifier을 간단하게 설명하자면 동일한 타입 bean 객체에 의존 객체를 명시해주어 value값으로 선언한 bean객체를 주입한다.

이제 모든 설정이 끝났다.

전체적인 디렉토리 구조는 이렇다.

구조

결과

실행 시킬시에 각각의 Schema에 TABLE이 생성된 것을 볼 수 있다.

TestDB

JpaDB