업무를 하다보면 마이크로 서비스도 구축하게 되고, 다중 데이터베이스도 처리해야하는 경우가 생긴다.

일반적으로 하나의 서비스에서 다중 데이터베이스에 접속하는 일이 없겠지만, 가끔 불가피하게 이런일이 생기게 된다.

그래서 시작된 삽질과 도출된 결과를 공유하고자 한다.

최종 테스트까지의 과정을 위해 몇 가지 준비를 해야한다.

다분히 기본적인 부분은 제외하고 작성하려한다.

build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dependencies {
implementation('org.springframework.boot:spring-boot-starter-web')
implementation('org.springframework.boot:spring-boot-starter-data-jpa')
compileOnly('org.springframework.boot:spring-boot-configuration-processor')

runtimeOnly('mysql:mysql-connector-java:')

compileOnly ("org.projectlombok:lombok:1.18.18")
annotationProcessor ("org.projectlombok:lombok:1.18.18")

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("junit:junit:4.12")
testCompileOnly ("org.projectlombok:lombok:1.18.18")
testAnnotationProcessor ("org.projectlombok:lombok:1.18.18")
}

Sprint boot, Mysql, Junit 을 기반으로 테스트 프로젝트를 구축한다.

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://db-url-A:3306/db-name
username: db-A-user
password: iampass
datasource-other:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://db-url-B:3306/db-name
username: db-B-user
password: admin
jpa:
properties:
hibernate:
hbm2ddl:
auto: validate
show_sql: true
format_sql: true
enable_lazy_load_no_trans: true
dialect: org.hibernate.dialect.MySQL5Dialect
database: mysql

두 데이터 베이스의 정보를 각각의 datasource 네임스페이스로 지정한다.

하이버네이트 설정은 예시이다. 이대로 할 필요는 없다.

데이터베이스 별 Entity와 Repository를 생성한 후 각각의 EntityManager를 구성할텐데
이쯤되면 왜 이렇게까지 JPA를 사용해야할까 라는 합리적 의심을 하시는 분들이 계실듯 하다.

이런 분들은 ORM, SQL Mappper 와 더불어 JDBC 까지 공부해보면 좋을것 같다.

Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//com.multiple.entity.a.AtypeEntity
@Getter
@Setter
@Entity
@Table(name="a_db_table")
public class AtypeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long idx;
@Column(name = "user_idx", nullable = false)
private int userIdx;
@Column(name = "name", nullable = false, length = 255)
private String name;
}

// com.multiple.entity.b.BtypeEntity
@Getter
@Setter
@Entity
@Table(name="b_db_table")
public class BtypeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "type_idx", nullable = false)
private long typeIdx;
@Column(length = 20 , nullable = false)
private String type;
@Column(length = 50)
private String description;
}

Repository

1
2
3
4
5
6
7
8
9
// com.multiple.repository.a.AtypeRepository
@Repository
public interface AtypeRepository extends JpaRepository<AtypeEntity, Long> {
}

// com.multiple.repository.b.BtypeRepository
@Repository
public interface BtypeRepository extends JpaRepository<BtypeEntity, Long> {
}

여기까지가 준비단계이다.

이제 본격적으로 데이터베이스별로 객쳐 연결 관리를 위한 설정작업을 시작해보자.

Primary Database Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// com.multiple.configuration.PersistenceAdbConfiguration
@Configuration
@EnableJpaRepositories(
basePackages = "com.multiple.repository.a",
entityManagerFactoryRef = "AdbEntityManager",
transactionManagerRef = "AdbTransactionManager"
)
public class PersistenceAdbConfiguration {
@Autowired
private Environment env; // 설정파일의 데이터베이스 정보를 가져온다.

/*@Bean
@Primary
public DataSource AdbDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();

final String sourceRoot = "spring.datasource";

dataSource.setDriverClassName(env.getProperty(sourceRoot + ".driver-class-name"));
dataSource.setUrl(env.getProperty(sourceRoot + ".url"));
dataSource.setUsername(env.getProperty(sourceRoot + ".username"));
dataSource.setPassword(env.getProperty(sourceRoot + ".password"));

return dataSource;
}*/

@Bean
@Primary
@ConfigurationProperties(prefix="spring.datasource")
public DataSourceProperties AdbDsProperties() {
return new DataSourceProperties();
}

@Bean
@Primary
public DataSource AdbDataSource(DataSourceProperties properties) {
//DataSourceBuilder.create().build();
return properties.initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
}

@Bean
@Primary
public LocalContainerEntityManagerFactoryBean AdbEntityManager() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();

HibernateJpaVendorAdapter hbAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(hbAdapter);

em.setDataSource(AdbDataSource(AdbDsProperties()));
em.setPackagesToScan("com.multiple.entity.a"); // database entity package path
em.setJpaProperties(jpaProperties());

return em;
}

@Bean
@Primary
public PlatformTransactionManager AdbTransactionManager(EntityManagerFactory emf) {
JpaTransactionManager transactionManager = new JpaTransactionManager();

transactionManager.setEntityManagerFactory(emf);

return transactionManager;
}

private Properties jpaProperties() {
Properties properties = new Properties();

final String rootPath = "spring.jpa.properties.hibernate";

properties.setProperty("hibernate.hbm2ddl.auto", env.getProperty(rootPath + ".hbm2ddl.auto"));
properties.setProperty("hibernate.dialect", env.getProperty(rootPath + ".dialect"));
properties.setProperty("hibernate.show_sql", env.getProperty(rootPath + ".show_sql"));
properties.setProperty("hibernate.format_sql", env.getProperty(rootPath + ".format_sql"));
properties.setProperty("hibernate.enable_lazy_load_no_trans",
env.getProperty(rootPath + ".enable_lazy_load_no_trans"));

return properties;
}
}

여기서 몇가지 짚고 넘어가자.

DataSource 생성 방법은 여러가지이므로 골라서 사용하되, Hikari(Connection Pool Framework) 사용 시

DataSourceBuilder에서 url이 바인딩이 안되는 경우가 생기므로

Property를 이용한 생성을 추천한다. Hikari가 default라고 생각하면 된다.

Hikari는 spring-boot-starter-data-jpa > spring-boot-starter-jdbc > HikariCP 을 통해 빌드된다.

EntityManager 는 Entity 객체와 DB를 연결 관리해주는 api 역할을 한다.

Secondary Database Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// com.multiple.configuration.PersistenceAdbConfiguration
@Configuration
@EnableJpaRepositories(
basePackages = "com.multiple.repository.b",
entityManagerFactoryRef = "BdbEntityManager",
transactionManagerRef = "BdbTransactionManager"
)
public class PersistenceBdbConfiguration {
@Autowired
private Environment env; // 설정파일의 데이터베이스 정보를 가져온다.

@Bean
@ConfigurationProperties(prefix="spring.datasource-other")
public DataSourceProperties BdbDsProperties() {
return new DataSourceProperties();
}

@Bean
public DataSource BdbDataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
}

@Bean
public LocalContainerEntityManagerFactoryBean BdbEntityManager() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();

HibernateJpaVendorAdapter hbAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(hbAdapter);

em.setDataSource(BdbDataSource(BdbDsProperties()));
em.setPackagesToScan("com.multiple.entity.b"); // database entity package path
em.setJpaProperties(jpaProperties());

return em;
}

@Bean
public PlatformTransactionManager BdbTransactionManager(@Autowired @Qualifier(value = "BdbEntityManager") EntityManagerFactory emf) {
JpaTransactionManager transactionManager = new JpaTransactionManager();

transactionManager.setEntityManagerFactory(emf);

return transactionManager;
}

private Properties jpaProperties() {
Properties properties = new Properties();

final String rootPath = "spring.jpa.properties.hibernate";

properties.setProperty("hibernate.hbm2ddl.auto", env.getProperty(rootPath + ".hbm2ddl.auto"));
properties.setProperty("hibernate.dialect", env.getProperty(rootPath + ".dialect"));
properties.setProperty("hibernate.show_sql", env.getProperty(rootPath + ".show_sql"));
properties.setProperty("hibernate.format_sql", env.getProperty(rootPath + ".format_sql"));
properties.setProperty("hibernate.enable_lazy_load_no_trans",
env.getProperty(rootPath + ".enable_lazy_load_no_trans"));

return properties;
}
}

이렇게 설정이 완료되었다.

테스트를 통해 확인!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RunWith(SpringRunner.class)
@SpringBootTest
@EnableTransactionManagement
public class MultipleJpaTest {
@Autowired
AtypeRepository aTypeRepository;

@Autowired
BtypeRepository bTypeRepository;

@Test
@Transactional("AdbTransactionManager")
public void AdbCount() {
Assert.isTrue(aTypeRepository.count() > 0, "find fail!");
}

@Test
@Transactional("BdbTransactionManager")
public void BdbCount() {
Assert.isTrue(bTypeRepository.count() > 0, "find fail!");
}
}

DB별로 DataSource, EntityManager, TrasactionManager를 설정값을 불러와서 생성 후

Entity, Repository와 연결해주는 작업으로 다중 데이터베이스를 객체화하여 사용할 수 있는 준비가 되었다.

참고문서