본문 바로가기

Language/Spring

[실무 - JPA] DB 다중화 연결

728x90

프로젝트 중 운영 DB와 통계 DB 두 개가 있는데, 운영 DB의 데이터를 분기(3개월)마다 통계 DB로 이관하여 Freezing을 해야하는 과업이 있었다.

현재 프로젝트에서는 운영 DB는 JPA와 Mybatis 두 개가 셋팅이 되어 있었으며, 통계 DB는 Mybatis만 셋팅이 되어있었고 운영 DB에서 JPA 또는 Mybatis로 Select를 하여 통계 DB에 Mybatis로 Insert를 해서 처리할 수도 있었지만, Mybatis에서 select를 할 때 resultType을 통계 DB의 Entity로 반환하여 JPA로 saveAll(조회데이터)를 하면 좀 더 수월할 것 같기도 하고, 이러한 설정을 경험해보는 것도 좋은 경험이 될 것 같다고 과장님이 제안을 해주셔서 흔쾌히 시작을 하게 되었다.

 

국지비원 학원과 항해99를 하면서 JPA를 사용해보기는 했지만 생각해보니 extends JpaRepository<Entity, Key>만 하면 알아서 DB에 CRUD가 되었기에 DB 연결 부분에 대해 의구심을 갖진 않았다.

 

하지만 실제 프로젝트에서는 분명 2개이상의 DB를 운용할텐데 어떤 기준으로 어느 경우에는 Main DB를, 어느 경우에는 Sub DB를 바라보게 할 것인가가 궁금해졌다.

 

개인 프로젝트 포폴을 만들면서 설정 파일인 application.yml 또는 application.properties에 DB Connection 정보를 작성하면 알아서 해당 DB 정보를 토대로 DB 연결이 되었었다.

 

하지만, DB가 2개이상이라면 A Package는 Main DB가 관리하게 하고, B Package는 Sub DB가 관리할 수 있게 설정을 해주어야 Framework가 정확하게 판단을 할 수 있게 되는 것 같다.

 

타 블로그의 예제를 참조하여 기본 셋팅을 해보았고, 이후 원하는 테스트들을 진행하였다


1-1. 기본 셋팅
  • 추가한 Dependency
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
  • application.yml
spring:
  datasource:
    hikari:
      #mysql - db1
      db1:
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/db1?&characterEncoding=UTF-8
        username: root
        password: "0000"

      #H2 - db2
      db2:
        driver-class-name: org.h2.Driver
        jdbc-url: jdbc:h2:mem:db2
        username: sa
        password:
  h2:
    console:
      enabled: true
      path: /h2-console
  jpa:
    hibernate:
      ddl-auto: update

    properties:
      hibernate:
        show_sql: true
        format_sql: true
  • 프로젝트 구조


1-2. db1Config & db2Config
package com.jpaproject.jpa_prj.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
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.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@EnableJpaRepositories (
        basePackages = "com.jpaproject.jpa_prj.db1Repository", // 이 경로의 Repository들은
        entityManagerFactoryRef = "db1EntityManager", // 이 EntityManagerFactory를 사용하고
        transactionManagerRef = "db1TransactionManager" // 이 TransactionManager를 사용함
)
@Configuration
public class db1Config {
    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hikari.db1")
    protected DataSource db1DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean db1EntityManager() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(db1DataSource());
        em.setPackagesToScan(new String[] {"com.jpaproject.jpa_prj.db1Entity"});
        em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());

        // yml 파일의 spring.jpa.* 설정은 기본 데이터소스 하나에만 적용이 된다고 함
        // 현재는 멀티 데이터소스 구성을 했기 때문에 db1EntityManager에 명시를 해줘야한다고 함
        // 아래 설정을 해주고 나서야, 프로젝트가 실행될 때 테이블을 생성해 줌
        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "update");
        properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
        properties.put("hibernate.show_sql", true);
        properties.put("hibernate.format_sql", true);
        em.setJpaPropertyMap(properties);

        return em;
    }

    @Primary
    @Bean
    public PlatformTransactionManager db1TransactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(db1EntityManager().getObject());

        return transactionManager;
    }
}
package com.jpaproject.jpa_prj.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
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.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@EnableJpaRepositories (
        basePackages = "com.jpaproject.jpa_prj.db2Repository",
        entityManagerFactoryRef = "db2EntityManager",
        transactionManagerRef = "db2TransactionManager"
)
@Configuration
public class db2Config {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hikari.db2")
    protected DataSource db2DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean db2EntityManager() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(db2DataSource());
        em.setPackagesToScan(new String[] {"com.jpaproject.jpa_prj.db2Entity"});
        em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());

        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "update");
        properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        properties.put("hibernate.show_sql", true);
        properties.put("hibernate.format_sql", true);
        em.setJpaPropertyMap(properties);

        return em;
    }

    @Bean
    public PlatformTransactionManager db2TransactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(db2EntityManager().getObject());

        return transactionManager;
    }
}

 

  • @EnableJpaRepositories()
    • Repository가 어떤 EntitiyManaerFactory(이하 EMF) 및 TransactionManager(이하 TM)를 사용할 지 지정하는 어노테이션
    • basePackage로 지정한 패키지에서 'extends JpaRepository<>'를 하는 interfaceConfig에서 설정한 DB에 연결
    • entityManagerFactoryRef와 transactionManagerRef 속성은 각각 이 Repository들이 사용할 EntityManagerFactory와 PlatformTransactionManager Bean의 이름을 지정
      → 해당 Config class에서의 선언된 메서드 이름과 일치(이름이 다르다면 @Bean(name="")을 통해 일치시켜 줌)
    • 즉, basePackage로 지정된 위치에서 JpaRepository를 상속한 Repository들은 해당 EM과 TM을 사용하여 연결된 DB와 연동
  • @ConfigurationProperties(prefix = "spring.datasource.hikari.db1")
    • yml 또는 properties에서 설정한 DB 설정 정보로 Datasource 생성
  • LocalContainerEntityManagerFactoryBean db1EntityManager()
    • em.setDataSource(데이터소스) : 위에서 생성한 DataSource를 EMF에 설정
    • em.setPackagesToSccan("경로") : 해당 Package 경로의 Entity를 관리
    • em.setJpaVendorAdapter() : JPA의 구현체가 Hibernate일 때, 이를 위한 설정을 JPA EMF에 전달해주는 역할이라고 하는데 이해가 잘 되진 않음
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(db1DataSource());
em.setPackagesToScan(new String[] {"com.jpaproject.jpa_prj.db1Entity"});
em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());

아래 설정 부분은 참조한 블로그에서는 없던 내용인데 주석의 내용처럼 아래 코드가 없었을 때, 자동으로 DB에 테이블을 생성하지 않아 Repository에서 save를 했을 때, db1 테이블이 없다는 에러가 발생하여 추가하였음

Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", "update");
properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
properties.put("hibernate.show_sql", true);
properties.put("hibernate.format_sql", true);
em.setJpaPropertyMap(properties);
  • PlatformTransactionManager db1TransactionManager()
    • 위에서 설정한 EMF를 TransactionManager에 설정
    • 트랜잭션을 시작하고, 커밋하고, 롤백하는 역할을 하는 관리자 역할
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(db1EntityManager().getObject());

1-3. DB1 Entity & DB2 Entity
package com.jpaproject.jpa_prj.db1Entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;

@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class DB1 {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String db1Col1;
    private String db1Col2;
    private String db1Col3;
    private String db1Col4;
}
package com.jpaproject.jpa_prj.db2Entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;

@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class DB2 {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    private String db2Col1;
    private String db2Col2;
    private String db2Col3;
    private String db2Col4;
}

1-4. Db1Repository & Db2Repository
package com.jpaproject.jpa_prj.db1Repository;

import com.jpaproject.jpa_prj.db1Entity.DB1;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface Db1Repository extends JpaRepository<DB1, Long> {
}
package com.jpaproject.jpa_prj.db2Repository;

import com.jpaproject.jpa_prj.db2Entity.DB2;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface Db2Repository extends JpaRepository<DB2, Long> {
}

1-5. Test
package com.jpaproject.jpa_prj;

import com.jpaproject.jpa_prj.db1Entity.DB1;
import com.jpaproject.jpa_prj.db1Repository.Db1Repository;
import com.jpaproject.jpa_prj.db2Entity.DB2;
import com.jpaproject.jpa_prj.db2Repository.Db2Repository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;

@SpringBootTest
class JpaPrjApplicationTests {
    @Autowired
    private Db1Repository db1Repository;
    @Autowired
    private Db2Repository db2Repository;

    @Test
    void main() {
        System.out.println("실행ㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇ");
        System.out.println(db1Repository.findAll());

        // ghlove에서는 mybatis를 이용해서, select를 할 것이고
        // select 시 ResultType을 Analysis의 GCntrTemp 같은 엔티티로 할 것임
        // 현재 테스트에선 DB1을 DB2로 변환하는 과정이 필요
        List<DB2> list = db1Repository.findAll().stream().map(
                db1 -> DB2.builder()
                        .db2Col1(db1.getDb1Col1())
                        .db2Col2(db1.getDb1Col2())
                        .db2Col3(db1.getDb1Col3())
                        .db2Col4(db1.getDb1Col4())
                        .build()
                ).toList();
        long currentTime = System.currentTimeMillis();
        db2Repository.saveAll(list);
        long endTime = System.currentTimeMillis();


        List<DB2> findList = db2Repository.findAll();
        System.out.println("1. 저장할 데이터 리스트 크기 = " + list.size());
        System.out.println("2. 저장에 소요된 시간  = " + (endTime - currentTime) + " ms");
        System.out.println("3. 저장된 데이터 리스크 크기 = " + findList.size());
    }

    @Test
    void makeData() {
        for(int i = 1; i <= 100000; i++) {
            init();
        }
    }

    void init() {
        for(int i = 1; i <= 5; i++) {
            DB1 db1 = DB1.builder()
                    .db1Col1("tmpData"+ i +"Col1")
                    .db1Col2("tmpData"+ i +"Col1")
                    .db1Col3("tmpData"+ i +"Col1")
                    .db1Col4("tmpData"+ i +"Col1")
                    .build();
            db1Repository.save(db1);
        }
    }

}

makeData()에서 5개의 데이터를 10만번 넣어 50만건의 데이터를 생성하려 했는데 생각보다 시간이 오래 걸려 237999건 정도의 DB 데이터로 테스트를 했고, mybatis의 resultType으로 통계 DB의 Entity로 꺼낼 수 있다는 가정하에매우 빠르지 않을까 생각이 듦

main() 메서드를 실행하였을 때, 총 25초정도가 소요가 되었었는데, saveAll()로 저장에 소요된 시간은 약 7~8초정도 밖에 소요가 안 됐고 나머지 시간이 DB1 엔티티를 DB2로 변환하는 시간이였음


2. 패키지 구조 변경

패키지 구조를 운영 DB는 main 패키지를 관리, 통계 DB는 analysis 패키지를 관리할 수 있도록 실무에 적용할 구조로 변경을 해보았음

단지 db1, db2의 이름을 각각 main, analysis로 변경했을 뿐이며, 패키지만 좀 더 명확히 구분을 함

Github 소스에 반영이 되어있진 않지만 다음의 코드처럼 하면 해당 패키지 전체를 관리할 수 있던 것으로 기억 함

package com.jpaproject.jpa_prj.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
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.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@EnableJpaRepositories (
        // basePackages = "com.jpaproject.jpa_prj.analysis.analysisDbRepository",
        basePackages = "com.jpaproject.jpa_prj.analysis",
        entityManagerFactoryRef = "analysisEntityManager",
        transactionManagerRef = "analysisTransactionManager"
)
@Configuration
public class AnalysisConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hikari.analysis")
    protected DataSource analysisDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean analysisEntityManager() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(analysisDataSource());
        // em.setPackagesToScan(new String[] {"com.jpaproject.jpa_prj.analysis.analysisDbEntity"});
        em.setPackagesToScan(new String[] {"com.jpaproject.jpa_prj.analysis"});
        em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());

        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "update");
        properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        properties.put("hibernate.show_sql", true);
        properties.put("hibernate.format_sql", true);
        em.setJpaPropertyMap(properties);

        return em;
    }

    @Bean
    public PlatformTransactionManager analysisTransactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(analysisEntityManager().getObject());

        return transactionManager;
    }
}

3. 관리 패키지 안에 관리 패키지

  • 현재 실무 프로젝트에서는 운영 DB의 basePackage가 main 패키지를 로 잡혀있는데, 통계 DB의 basePackage가 main.analysis로 잡혀있음
  • 결론적으로 말하자면, 운영 DB 한 개만 JPA를 사용했기 때문에 문제가 안 되었던 것 같은데 위의 구조로 통계 DB에 @EnableJpaRepositories 어노테이션을 추가하니 충돌이 난 것 같음
  • 이미 main Package 전체를 운영 DB로 묶어두었는데, 이미 관리 중인 것을 통계 DB가 관리하는 것은 불가능한 것으로 보임
  • 즉, basePackage의 범위가 겹치지 않게 패키지를 잘 나누어야 함

참조

https://suzuworld.tistory.com/m/327

 

JAVA + Spring Data JPA 프로젝트에 다중 DB를 연결해보자.

💡 회사에서 새롭게 서버를 리팩터링 하는 도중에 하나의 프로젝트에 2개 이상의 데이터베이스를 연결해야 했다. 구글링을 통해 검색해 본 결과 생각보다 간단하게 할 수 있었다. 집에 와서 프

suzuworld.tistory.com

 

https://github.com/calmnature/multiJpaConnection

 

GitHub - calmnature/multiJpaConnection

Contribute to calmnature/multiJpaConnection development by creating an account on GitHub.

github.com

 

728x90