하나의 애플리케이션에서 여러 데이터베이스에 붙어 작업해야하는 경우가 있습니다.
ex) 라이브DB 이전 작업, 기능 이전 작업 등
오늘은 멀티 데이터베이스를 하나의 스프링 애플리케이션에서 붙어 JPA로 작업할 수 있도록 하겠습니다.
두 개의 데이터베이스를 띄우기 위해 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'
여러 데이터베이스에 붙기 위해 각 데이터베이스 별 설정파일 정보를 기입합니다.
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
@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 등
각 데이터베이스에서 사용할 엔티티의 패키지 스캔 범위를 지정해주어야 합니다.
해당 패키지를 포함해서 하위 모든 패키지의 컴포넌트를 스캔합니다.
엔티티 매니저 팩토리 설정 시 하이버네이트 설정 내용을 기입합니다.
해당 패키지에 있는 엔티티는 별도의 트랜잭션 매니저를 가지도록 설정합니다.
@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);
}
}
@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);
}
}
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());
}
패키지 스캔 별로 별도의 트랜잭션 매니저를 사용할 필요가 있습니다. 다음과 같이 적절한 트랜잭션 매니저를 사용해주세요.
위와 같은 설정대로 한다면 적절한 트랜잭션 매니저를 사용하지 않거나 하나의 트랜잭션에서 두 데이터베이스에 대한 CUD 수행 시 일관성을 보장하지 못합니다.
별도의 트랜잭션 매니저를 사용해야 하는 서로 다른 엔티티들을 하나의 트랜잭션에서 처리하기 위해 처음에 ChainedTransactionManager
를 알아보았습니다.
하지만 이는 Two Phase Commit
을 해주는 것이 아니며 완벽한 일관성을 보장하지 못한다는 것을 알게 됐습니다.
TCC를 해주지 않기 때문에 ChainedTransactionManager 는 다음과 같은 상황에서 일관성을 보장해주지 못하며 Deprecated 되었습니다.
이에 대한 대안으로 JtaTransactionManager가 나왔습니다.
Jta 설정 예제는 다음 게시글에서 소개하겠습니다.