본문 바로가기
[JAVA]/JAVA 기본

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

by 팡펑퐁 2023. 5. 23.
728x90
💡 회사에서 새롭게 서버를 리팩터링 하는 도중에 하나의 프로젝트에 2개 이상의 데이터베이스를 연결해야 했다. 구글링을 통해 검색해 본 결과 생각보다 간단하게 할 수 있었다. 집에 와서 프로젝트를 간단하게 요약하여 재구성한 후 테스트를 진행해보려고 한다.

 

Spring Initializr

  • Spring Initializr 설정이다.
  • 원래 H2로도 테스트를 해보고 싶었는데 못해서 의미가 없게 됐다.

 

 

프로젝트 패키지 구조

  • 이 테스트의 핵심은 데이터베이스 2개를 하나의 프로젝트에 연결하고, 패키지 별로 사용 db를 나눈 후에 이를 기준으로 정상적으로 데이터베이스와 해당 패키지의 클래스가 연결되는지 보는 것이다.
  • 따라서 db1(main), db2(second) 서버와 연결할 각각의 Entity와 Repository 만들었다.
  • 또, Entity&Repository와 각 데이터베이스의 연결을 담당할 configuration 클래스도 각각 만들었다.

 

 

application.yml

#mysql - db1
spring:
  datasource:
    hikari:
      db1:
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/db1?&characterEncoding=UTF-8
        username: mysql username 입력
        password: 비밀번호 입력

#mysql - db2
      db2:
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/db2?&characterEncoding=UTF-8
        username: mysql username 입력
        password: 비밀번호 입력

  jpa:
    hibernate:
      ddl-auto: create # 애플리케이션이 정상 작동하면 기존 테이블을 지우고 엔티티를 읽어 새로 만듬

    properties:
      hibernate:
        show_sql: true # sql문이 db로 날라가는 것을 system.out으로 확인 가능
        format_sql: true # 한줄이 아닌 여러 줄로 나뉘어 sql문의 가독성을 높여서 볼 수 있음
  • 두 개의 DB를 연결해 주었다.
  • 이제 confiuration 클래스를 만들어 Repository와 Entity 별로 원하는 DB와 연결하기 위한 작업을 진행해야 한다.

 

 

db1Config

package Test.MultiDb.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;

@EnableJpaRepositories(
        basePackages = "Test.MultiDb.db1Repository",  // 적용할 repository 경로
        entityManagerFactoryRef = "db1EntityManager",  // (1) 아래 메서드 명과 일치해야 함
        transactionManagerRef = "db1TransactionManager"  // (2) 아래 메서드 명과 일치해야 함
)
@Configuration
public class db1Config {
    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean db1EntityManager() {  // (1)
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(db1DataSource());
        em.setPackagesToScan(new String[] {"Test.MultiDb.db1Entity"});
        em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        return em;
    }

    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hikari.db1") // 적용할 Entity 경로
    protected DataSource db1DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean
    public PlatformTransactionManager db1TransactionManager() {  // (2)
        JpaTransactionManager transactionManager = new JpaTransactionManager();

        transactionManager.setEntityManagerFactory(db1EntityManager().getObject());
        return transactionManager;
    }
}
  • db1 데이터베이스와 db1의 Entity & Repository와 연결을 설정하는 클래스이다.

@EnableJpaRepositories

  • JPA Repository Bean을 활성화한다.
  • basePackages를 통해 적용할 레파지토리의 패키지를 여러 곳 적을 수 있다. db별로 Repository를 나누어 관리해야 함을 의미한다.

@Primary

  • 동일한 인터페이스를 구현하거나 동일한 클래스를 확장하는 빈 객체가 여러 개일 경우 Primary 애너테이션이 붙은 빈이 우선순위를 갖는다.
  • 여기서는 예시로 db1을 메인 db로 둔다는 가정을 했기 때문에 명시했다.

LocalContainerEntityManagerFactoryBean

  • Datasource, Hibernate Property, Entity가 위치한 Package를 지정한다.
  • Hibernate 기반으로 동작을 지정하는 JpaVendor를 설정한다.
  • Hibernate vendor와 JPA 간 Adapter를 설정한다.
  • Hibernate vendor는 Hibernate 관련 속성 및 동작을 담당한다.

@ConfigurationProperties

  • *.properties, *.yml 파일에 있는 설정을 자바클래스에 가져와 사용할 수 있게 한다.

Datasource

  • 사용자 지정 데이터 소스를 사용하기 위해 정의한다.

PlatformTransactionManager

  • DB 연동에 따라 각각의 구현 클래스가 제공된다.
  • @Transactional이 포함된 메서드가 호출될 경우에 PlatformTransactionManager를 사용하여 트랜잭션을 시작하고 정상 작동 여부에 따라 Commit 또는 Rollback을 한다.

 

 

db2Config

package Test.MultiDb.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;

@EnableJpaRepositories(
        basePackages = "Test.MultiDb.db2Repository",
        entityManagerFactoryRef = "db2EntityManager",
        transactionManagerRef = "db2TransactionManager"
)
@Configuration
public class db2Config {
    @Bean
    public LocalContainerEntityManagerFactoryBean db2EntityManager() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(db2DataSource());
        em.setPackagesToScan(new String[] {"Test.MultiDb.db2Entity"});
        em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        return em;
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hikari.db2")
    protected DataSource db2DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public PlatformTransactionManager db2TransactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();

        transactionManager.setEntityManagerFactory(db2EntityManager().getObject());
        return transactionManager;
    }
}
  • db1 데이터베이스와 db1의 Entity & Repository와 연결을 설정하는 클래스이다.
  • 자세한 설명은 dbConfig2로 대신한다.

 

 

DB1 & DB2 Entity

package Test.MultiDb.db1Entity;

import lombok.Getter;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
@Getter
public class DB1 {
    @Id
    @GeneratedValue
    private Long id;

    private String db1Col1;
    private String db1Col2;
    private String db1Col3;
    private String db1Col4;
}

//-------------------------------------------

package Test.MultiDb.db2Entity;

import lombok.Getter;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
@Getter
public class DB2 {
    @Id
    @GeneratedValue
    private Long id;

    private String db2Col1;
    private String db2Col2;
    private String db2Col3;
    private String db2Col4;
}
  • db1, db2 테이블과 매핑할 엔티티이다.

 

 

package Test.MultiDb.db1Repository;

import Test.MultiDb.db1Entity.DB1;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface Db1Repository extends JpaRepository<DB1, Long> {
}


//----------------------------------------------------------------


package Test.MultiDb.db2Repository;

import Test.MultiDb.db2Entity.DB2;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface Db2Repository extends JpaRepository<DB2, Long> {
}
  • JPaRepository를 상속받은 DB1Repository, DB2Repository이다.

 

 

MySQL DB 설정

  • MySQL 내 테이블에 데이터가 들어있는 상황을 가정했다.
  • 데이터베이스와 테이블을 만들고 스텁 데이터를 넣는 작업을 했다.

create database db1;
create database db2;
  • create 명령문으로 데이터베이스 생성

 

 

use db1;
show tables;
  • use db1 명령문 이용하여 db1 데이터베이스 접속
  • show tables 명령문을 이용하여 db1 데이터베이스 내 테이블 확인(비어있음)

 

 

  • create table 명령문으로 db1 데이터베이스에 db1 테이블을 생성
  • insert into db1 명령문을 이용해 db1 테이블에 스텁데이터 삽입

 

 

  • db2 데이터베이스에도 동일하게 db2 테이블 생성 후 스텁데이터 삽입

 

 

테스트코드 작성

package Test.MultiDb;


import Test.MultiDb.db1Entity.DB1;
import Test.MultiDb.db1Repository.Db1Repository;
import Test.MultiDb.db2Entity.DB2;
import Test.MultiDb.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;

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

	@Test
	void init() {
		testDB1();
		System.out.println("----------------------------------");
		testDB2();
	}

	void testDB1() {
		List<DB1> all = db1Repository.findAll();
		for (DB1 db1 : all) {
			System.out.println(db1.getId());
			System.out.println(db1.getDb1Col1());
		}
	}

	void testDB2() {
		List<DB2> all = db2Repository.findAll();
		for (DB2 db2 : all) {
			System.out.println(db2.getId());
			System.out.println(db2.getDb2Col1());
		}
	}
}
  • 준비가 끝났다. 테스트를 진행해 보자.
  • init() 테스트를 실행하면 testDB1()와 textDB2()가 차례로 실행된다.
  • 각각의 레파지토리에서 스텁데이터를 가져와 id와 첫 번째 칼럼의 데이터를 출력하는 테스트이다.
  • 정상적으로 작동한다면 서로 다른 db인 db1과 db2에서 데이터를 가져와 '-----' 선을 기준으로 id와 칼럼에 들어있는 스텁데이터가 출력될 것이다.
  • 양쪽 db에 동일한 데이터를 같은 개수로 넣었으니 정상적으로 테스트가 작동한다면 동일한 출력문이 2번 반복될 것이다.

 

 

출력 화면

  • 하나의 프로젝트로 양쪽 db에 접근하여 데이터를 다룰 수 있음이 확인되었다.

 

자세한 코드는 깃허브에 올려두었다.

https://github.com/wonyongg/test/tree/main/MultiDb

 

 

 

참고

https://frogand.tistory.com/132

https://kitty-geno.tistory.com/168

728x90