Spring Data JPA multi-datasource 설정하기 #1

ssongkim·2023년 7월 8일
2

Spring

목록 보기
1/2

Overview

하나의 애플리케이션에서 여러 데이터베이스에 붙어 작업해야하는 경우가 있습니다.
ex) 라이브DB 이전 작업, 기능 이전 작업 등

오늘은 멀티 데이터베이스를 하나의 스프링 애플리케이션에서 붙어 JPA로 작업할 수 있도록 하겠습니다.

1. docker-compose 작성

두 개의 데이터베이스를 띄우기 위해 docker-compose를 작성합니다.

version: '3'
services:
  db-1:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test1
    ports:
      - '3306:3306'
  db-2:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test2
    ports:
      - '3307:3306'

2. application.yml 작성

여러 데이터베이스에 붙기 위해 각 데이터베이스 별 설정파일 정보를 기입합니다.

spring:
  h2:
    console:
      enabled: true
      path: /h2
  datasource:
    one:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/test1?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC&allowPublicKeyRetrieval=true
      username: root
      password: root
      hikari:
        auto-commit: false
        connection-test-query: SELECT 1
        maximum-pool-size: 10
        pool-name: mysql-example-cp
      hibernate:
        ddl-auto: update
    two:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3307/test2?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC&allowPublicKeyRetrieval=true
      username: root
      password: root
      hikari:
        auto-commit: false
        connection-test-query: SELECT 1
        maximum-pool-size: 10
        pool-name: mysql-example2-cp
      hibernate:
        ddl-auto: update
  jpa:
    show-sql: true
    generate-ddl: false

    properties:
      hibernate:
        format_sql: true

3. DatabaseProperties 작성

@Data
@ConfigurationProperties(prefix = "spring.datasource")
public class DatabaseProperties {
  private One one;
  private Two two;

  @Data
  public static class One {
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private Hibernate hibernate;

  }

  @Data
  public static class Two {
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private Hibernate hibernate;

  }

  @Data
  public static class Hibernate {
    private String ddlAuto;
    private String dialect;
    private Naming naming;

    public static Map<String, Object> propertiesToMap(Hibernate hibernateProperties) {
      Map<String, Object> properties = new HashMap<>();

      if(hibernateProperties.getDdlAuto() != null) {
        properties.put("hibernate.hbm2ddl.auto", hibernateProperties.getDdlAuto());
      }

      DatabaseProperties.Naming hibernateNaming = hibernateProperties.getNaming();
      if(hibernateNaming != null) {
        if (hibernateNaming.getImplicitStrategy() != null) {
          properties.put("hibernate.implicit_naming_strategy",  hibernateNaming.getImplicitStrategy());
        }
        if (hibernateNaming.getPhysicalStrategy() != null) {
          properties.put("hibernate.physical_naming_strategy", hibernateNaming.getPhysicalStrategy());
        }
      }

      return properties;
    }
  }

  @Data
  public static class Naming {
    private String implicitStrategy;
    private String physicalStrategy;
  } } 

앞서 설정한 database 설정 정보를 객체에 매핑해서 사용하기 위해 DatabaseProperties를 작성합니다.

spring.jpa.hibernate.* 에 해당하는 설정값은 기본적으로 자동할당이 되지 않으니 임의로 설정파일에서 값을 지정해주어야합니다. ex) spring.jpa.hibernate.ddl-auto 등

4. Database Configuration 작성

각 데이터베이스에서 사용할 엔티티의 패키지 스캔 범위를 지정해주어야 합니다.
해당 패키지를 포함해서 하위 모든 패키지의 컴포넌트를 스캔합니다.
엔티티 매니저 팩토리 설정 시 하이버네이트 설정 내용을 기입합니다.
해당 패키지에 있는 엔티티는 별도의 트랜잭션 매니저를 가지도록 설정합니다.

주DB 설정

@Configuration
@EnableConfigurationProperties(DatabaseProperties.class)
@EnableJpaRepositories(basePackages = {"com.example.javajpamultidatabaseexample.account", "com.example.javajpamultidatabaseexample.board"},
                       entityManagerFactoryRef = PersistenceDBOneConfiguration.ENTITY_MANAGER_BEAN_NAME,
                       transactionManagerRef = PersistenceDBOneConfiguration.TRANSACTION_MANAGER_BEAN_NAME)
public class  PersistenceDBOneConfiguration {
  public static final String TRANSACTION_MANAGER_BEAN_NAME = "oneDBTransactionManager";
  public static final String ENTITY_MANAGER_BEAN_NAME = "oneDBEntityManager";
  private static final String DATASOURCE_BEAN_NAME = "oneDataSource";
  private static final String DATASOURCE_PROPERTIES_PREFIX = "spring.datasource.one";
  private static final String DATASOURCE_PROPERTIES = "oneDataSourceProperties";
  private static final String HIBERNATE_PROPERTIES = "oneHibernateProperties";

  @Primary
  @Bean(name = ENTITY_MANAGER_BEAN_NAME)
  public LocalContainerEntityManagerFactoryBean entityManager(EntityManagerFactoryBuilder builder, @Qualifier(DATASOURCE_BEAN_NAME) DataSource dataSource,
      @Qualifier(HIBERNATE_PROPERTIES) DatabaseProperties.Hibernate hibernateProperties) {

    return builder.dataSource(dataSource).packages("com.example.javajpamultidatabaseexample.account", "com.example.javajpamultidatabaseexample.board")
        .persistenceUnit(ENTITY_MANAGER_BEAN_NAME)
        .properties(DatabaseProperties.Hibernate.propertiesToMap(hibernateProperties)).build();   }

  @Bean(name = HIBERNATE_PROPERTIES)
  @ConfigurationProperties(DATASOURCE_PROPERTIES_PREFIX + ".hibernate")
  public DatabaseProperties.Hibernate hibernateProperties() {
    return new DatabaseProperties.Hibernate();
  }

  @Bean(name = DATASOURCE_PROPERTIES)
  @ConfigurationProperties(DATASOURCE_PROPERTIES_PREFIX)
  public DataSourceProperties dataSourceProperties() {
    return new DataSourceProperties();
  }

  @Primary
  @Bean(name = DATASOURCE_BEAN_NAME)
  @ConfigurationProperties(prefix = DATASOURCE_PROPERTIES_PREFIX + ".hikari")
  public DataSource dataSource(@Qualifier(DATASOURCE_PROPERTIES) DataSourceProperties dataSourceProperties) {
    return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
  }

  @Primary
  @Bean(name = TRANSACTION_MANAGER_BEAN_NAME)
  public PlatformTransactionManager transactionManager(@Qualifier(ENTITY_MANAGER_BEAN_NAME) EntityManagerFactory entityManagerFactory) {
    return new JpaTransactionManager(entityManagerFactory);
  }
}

이외 DB 설정

@Configuration
@EnableConfigurationProperties(DatabaseProperties.class)
@EnableJpaRepositories(basePackages = {"com.example.javajpamultidatabaseexample.school"},
                       entityManagerFactoryRef =  PersistenceDBTwoConfiguration .ENTITY_MANAGER_BEAN_NAME,
                       transactionManagerRef =  PersistenceDBTwoConfiguration .TRANSACTION_MANAGER_BEAN_NAME)
public class  PersistenceDBTwoConfiguration  {
  public static final String TRANSACTION_MANAGER_BEAN_NAME = "twoDBTransactionManager";
  public static final String ENTITY_MANAGER_BEAN_NAME = "twoDBEntityManager";
  private static final String DATASOURCE_BEAN_NAME = "twoDataSource";
  private static final String DATASOURCE_PROPERTIES = "twoDataSourceProperties";
  private static final String DATASOURCE_PROPERTIES_PREFIX = "spring.datasource.two";
  private static final String HIBERNATE_PROPERTIES = "twoHibernateProperties";

  @Bean(name = ENTITY_MANAGER_BEAN_NAME)
  public LocalContainerEntityManagerFactoryBean entityManager(EntityManagerFactoryBuilder builder, @Qualifier(DATASOURCE_BEAN_NAME) DataSource dataSource,
      @Qualifier(HIBERNATE_PROPERTIES) DatabaseProperties.Hibernate hibernateProperties) {

    return builder.dataSource(dataSource).packages("com.example.javajpamultidatabaseexample.school").persistenceUnit(ENTITY_MANAGER_BEAN_NAME)
        .properties(DatabaseProperties.Hibernate.propertiesToMap(hibernateProperties)).build();   }

  @Bean(name = HIBERNATE_PROPERTIES)
  @ConfigurationProperties(DATASOURCE_PROPERTIES_PREFIX + ".hibernate")
  public DatabaseProperties.Hibernate hibernateProperties() {
    return new DatabaseProperties.Hibernate();
  }

  @Bean(name = DATASOURCE_PROPERTIES)
  @ConfigurationProperties(DATASOURCE_PROPERTIES_PREFIX)
  public DataSourceProperties dataSourceProperties() {
    return new DataSourceProperties();
  }

  @Bean(name = DATASOURCE_BEAN_NAME)
  @ConfigurationProperties(prefix = DATASOURCE_PROPERTIES_PREFIX + ".hikari")
  public DataSource dataSource(@Qualifier(DATASOURCE_PROPERTIES) DataSourceProperties dataSourceProperties) {
    return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
  }

  @Bean(name = TRANSACTION_MANAGER_BEAN_NAME)
  public PlatformTransactionManager transactionManager(@Qualifier(ENTITY_MANAGER_BEAN_NAME) EntityManagerFactory entityManagerFactory) {
    return new JpaTransactionManager(entityManagerFactory);
  }
}

Test 해보기

ddl-auto가 create

Hibernate: 
    drop table if exists account
Hibernate: 
    drop table if exists board
Hibernate: 
    create table account (
        account_id bigint not null auto_increment,
        username varchar(50),
        password varchar(100),
        primary key (account_id)
    ) engine=InnoDB
Hibernate: 
    create table board (
        board_id bigint not null auto_increment,
        content varchar(255),
        title varchar(255),
        primary key (board_id)
    ) engine=InnoDB
Hibernate: 
    alter table account 
       add constraint UK_gex1lmaqpg0ir5g1f5eftyaa1 unique (username)

Hibernate: 
    alter table school_class 
       drop 
       foreign key FK2br5afl4106t79kv6m2bgwu8b
Hibernate: 
    drop table if exists school
Hibernate: 
    drop table if exists school_class
Hibernate: 
    create table school (
        school_id bigint not null auto_increment,
        school_name varchar(255),
        primary key (school_id)
    ) engine=InnoDB
Hibernate: 
    create table school_class (
        school_class_id bigint not null auto_increment,
        school_id bigint,
        name varchar(255),
        primary key (school_class_id)
    ) engine=InnoDB
Hibernate: 
    alter table school_class 
       add constraint FK2br5afl4106t79kv6m2bgwu8b 
       foreign key (school_id) 
       references school (school_id)
2023-06-19T10:16:00.977+09:00  INFO 28500 --- [           main] o.h.t.s.i.e.GenerationTargetToDatabase   : HHH000476: Executing script '[injected ScriptSourceInputNonExistentImpl script]'
2023-06-19T10:16:00.977+09:00  INFO 28500 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'twoDBEntityManager'
2023-06-19T10:16:01.148+09:00  WARN 28500 --- [           main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-06-19T10:16:01.330+09:00  INFO 28500 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-06-19T10:16:01.334+09:00  INFO 28500 --- [           main] j.JavaJpaMultidatabaseExampleApplication : Started JavaJpaMultidatabaseExampleApplication in 2.489 seconds (process running for 2.823)

데이터베이스 별로 엔티티가 생성됐습니다.

사용하기

@Service
@RequiredArgsConstructor
public class AccountService {
  private final AccountRepository accountRepository;

  @Transactional("oneDBTransactionManager")
  public void save(String username, String password) {
    accountRepository.save(Account.builder()
            .username(username)
            .password(password)
        .build());
  }

패키지 스캔 별로 별도의 트랜잭션 매니저를 사용할 필요가 있습니다. 다음과 같이 적절한 트랜잭션 매니저를 사용해주세요.

Two Phase Commit을 지원하는가?

위와 같은 설정대로 한다면 적절한 트랜잭션 매니저를 사용하지 않거나 하나의 트랜잭션에서 두 데이터베이스에 대한 CUD 수행 시 일관성을 보장하지 못합니다.

별도의 트랜잭션 매니저를 사용해야 하는 서로 다른 엔티티들을 하나의 트랜잭션에서 처리하기 위해 처음에 ChainedTransactionManager 를 알아보았습니다.

하지만 이는 Two Phase Commit 을 해주는 것이 아니며 완벽한 일관성을 보장하지 못한다는 것을 알게 됐습니다.

TCC를 해주지 않기 때문에 ChainedTransactionManager 는 다음과 같은 상황에서 일관성을 보장해주지 못하며 Deprecated  되었습니다.

이에 대한 대안으로 JtaTransactionManager가 나왔습니다.

Jta 설정 예제는 다음 게시글에서 소개하겠습니다.

profile
鈍筆勝聰✍️

0개의 댓글