MSA로 프로젝트를 진행하던 중, CQRS를 위해 CUD를 하는 DB와 Read를 위한 DB를 따로 두자는 이야기가 나왔다. 이렇게 DB를 2개 두면서 CUD를 위한 DB를 연결하는 작업과 Read를 위한 DB를 연결하는 작업을 따로 하게 되었다. 같은 Entity를 사용해도 상황에 따라 다른 DB에서 데이터를 불러와야 했어서 이걸 어떻게 적용해야 할지 고민이 됐는데, 역시 구글에 다양한 방법이 나와 있었다. 구글링 최고..! 그동안 application.yml을 통해 Datasource를 하나만 연결해서 사용해 왔는데 이렇게 여러 개를 연결하는 건 처음이라 진행한 방식을 기록하려 한다.
CommandDB : CUD를 위한 DB
QueryDB : Read를 위한 DB
@Repository
public interface MemberQueryRepository extends JpaRepository<Member, Long> {
Optional<Member> findByIdAndState(Long memberId, State state);
}
@EnableJpaRepositories
는 Config 파일에서 작성하기 때문에 Repository에는 따로 작성하지 않아도 된다. 만약 @EnableJpaRepositories
를 작성할 경우 Bean 중복 에러가 발생한다.
@Entity
public class Member extends BaseEntity {
@NotNull
private Date birth;
private String about;
@NotNull
private String nickname;
private String profileImage;
@NotNull
private String socialId;
@NotNull
private String socialType;
@Enumerated(EnumType.STRING) @NotNull
private State state;
}
각 Datasource가 다른 Entity와 연결되어 있는 경우에는 Repository를 하나씩만 작성하면 된다. 하지만 만약 Datasource가 같은 Entity와 연결되어 있는 경우에는 Datasource별로 Repository를 따로 작성해야 한다.
나는 같은 Entity에서 2개의 DB를 사용했기 때문에 command 폴더, query 폴더를 따로 만들어서 Repository를 각각 두 개씩 작성해 주었다. 이때 경로가 달라도 파일 이름이 같으면 Bean 중복 에러가 발생하기 때문에 CommandRepository
, QueryRepository
이런 식으로 파일명을 다르게 작성해 주었다.
spring:
application:
name: Lucky7-PostService
# Database
datasource:
query:
jdbc-url: ${LOCAL-QUERY-DB-ADDRESS}
username: ${LOCAL-QUERY-DB-HOST}
password: ${LOCAL-QUERY-DB-PASSWORD}
command:
jdbc-url: ${LOCAL-COMMAND-DB-ADDRESS}
username: ${LOCAL-COMMAND-DB-HOST}
password: ${LOCAL-COMMAND-DB-PASSWORD}
# JPA
jpa:
database-platform: org.mariadb.jdbc.Driver
hibernate:
ddl-auto: create
naming:
physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
properties:
hibernate:
dialect: org.hibernate.dialect.MariaDBDialect
format_sql: true
show_sql: false
yml파일에 각 Datasource를 구분할 수 있는 이름을 작성한다. 작성한 이름이 중복되면 인식을 하지 못하니 반드시 이름을 다르게 작성해 주어야 한다.
query:
jdbc-url: ${LOCAL-QUERY-DB-ADDRESS}
username: ${LOCAL-QUERY-DB-HOST}
password: ${LOCAL-QUERY-DB-PASSWORD}
나머지는 jdbc-url, username, password에 Datasource 한 개 연결하던 것과 같이 정보를 적어주면 된다. 기존과 다른 점은 url이 아닌 jdbc-url로 작성해야 에러가 발생하지 않는다는 것이다!
추가로 나는 Datasource를 여러 개 연결하면서 자동으로 Camel Case → Snake Case로 바뀌는 게 적용되지 않는 문제가 발생했다. 이 문제는 yml 파일에 아래 코드를 추가하는 것으로 해결하였다.
naming:
physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
@Configuration
@EnableJpaRepositories(
basePackages = {
"com.example.lucky7postservice.src.command"
},
entityManagerFactoryRef = "primaryEntityManager",
transactionManagerRef = "primaryTransactionManager"
)
public class CommandDataSource {
// Datasource 정보를 불러온다
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource.command")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
// EntityManagerFactory를 정의한다
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean primaryEntityManager() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(primaryDataSource());
em.setPackagesToScan(
"com.example.lucky7postservice.src.command.comment.domain",
"com.example.lucky7postservice.src.command.like.domain",
"com.example.lucky7postservice.src.command.post.domain"
);
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
HashMap<String, Object> prop = new HashMap<>();
prop.put("properties.hibernate.dialect", "org.hibernate.dialect.MariaDBDialect");
prop.put("hibernate.hbm2ddl.auto", "none");
prop.put("hibernate.format_sql", true);
prop.put("hibernate.show_sql", false);
prop.put("hibernate.physical_naming_strategy", "org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy");
em.setJpaPropertyMap(prop);
return em;
}
// TransactionManager를 정의한다
@Bean
@Primary
public PlatformTransactionManager primaryTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(primaryEntityManager().getObject());
return transactionManager;
}
}
주로 사용할 Datasource에 @Primary
를 붙여줘야 한다. @Primary
어노테이션은 중복으로 작성할 수는 없으며, 그렇지 않으면 에러가 발생한다.
@Configuration
@EnableJpaRepositories(
basePackages = {
"com.example.lucky7postservice.src.query.repository"
},
entityManagerFactoryRef = "secondEntityManager",
transactionManagerRef = "secondTransactionManager"
)
public class QueryDataSource {
// Datasource 정보를 불러온다
@Bean
@ConfigurationProperties(prefix = "spring.datasource.query")
public DataSource secondDataSource() {
return DataSourceBuilder.create().build();
}
// EntityManagerFactory를 정의한다
@Bean
public LocalContainerEntityManagerFactoryBean secondEntityManager() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(secondDataSource());
em.setPackagesToScan(
"com.example.lucky7postservice.src.query.member",
"com.example.lucky7postservice.src.command.comment.domain",
"com.example.lucky7postservice.src.command.like.domain",
"com.example.lucky7postservice.src.command.post.domain"
);
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
HashMap<String, Object> prop = new HashMap<>();
prop.put("properties.hibernate.dialect", "org.hibernate.dialect.MariaDBDialect");
prop.put("hibernate.hbm2ddl.auto", "none");
prop.put("hibernate.format_sql", true);
prop.put("hibernate.show_sql", false);
prop.put("hibernate.physical_naming_strategy", "org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy");
em.setJpaPropertyMap(prop);
return em;
}
// TransactionManager를 정의한다
@Bean
public PlatformTransactionManager secondTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(secondEntityManager().getObject());
return transactionManager;
}
}
@EnableJpaRepositories(
basePackages = {
"com.example.lucky7postservice.src.command"
},
entityManagerFactoryRef = "primaryEntityManager",
transactionManagerRef = "primaryTransactionManager"
)
basePackages
에 Datasource와 연결된 Repository가 있는 경로를 입력한다. 경로는 여러 개 입력 가능하며, Repository가 있는 모든 경로를 입력하면 된다.
entityManagerFactoryRef
, transactionManagerRef
에는 밑에 정의한 Bean을 각각 적어주면 된다.
@Bean
@ConfigurationProperties(prefix = "spring.datasource.query")
public DataSource secondDataSource() {
return DataSourceBuilder.create().build();
}
prefix에 application.yml 파일에 정의한 Datasource의 경로를 적어준다. 여기에서 yml 파일에 정의된 Datasource의 정보를 가져온다.
em.setPackagesToScan(
"com.example.lucky7postservice.src.query.member",
"com.example.lucky7postservice.src.command.comment.domain",
"com.example.lucky7postservice.src.command.like.domain",
"com.example.lucky7postservice.src.command.post.domain"
);
setPackagesToScan
에 연결된 Entity가 있는 경로를 입력한다. basePackages
와 마찬가지로 연결하고 싶은 Entity가 있는 모든 경로를 입력하면 된다. QueryDB의 경우 CommandDB에서 사용한 Entity에 추가로 QueryDB에만 있는 Entity가 존재하는 경로를 입력해 주었다. 이렇게 하면 같은 Entity를 이용해서 여러 개의 Datasource를 연결할 수 있다.
추가로, EntityManagerFactory에 JPA 설정을 해야 실제로 적용이 된다. application.yml에 한 것만으로는 적용이 안 되기 때문에 여기에 한 번 더 작성해 줘야 한다.
이렇게 하면 하나의 프로젝트에서 여러 개의 Datasource를 연결해서 사용할 수 있다. Datasource마다 Config 파일을 작성해 주면 되니 2개가 아닌 N개도 가능할 것 같다!
JPA Multiple DataSource 설정 방법
[JPA] 다중 Datasource 구성하기 (Spring boot Multi Datasource / DB 여러개 설정)
[JPA] Multi Datasource 설정하기
[SpringBoot JPA] 다중 DB 설정하기 (multi Datasource + 이기종 DB)
[Spring Boot] multiple DataSource 다중 데이터베이스 연결 구성
[Spring Boot] Bean 중복 등록 에러 해결 방법
JPA Naming Strategy, To upper snake case
[Spring] Spring Data JPA 사용 시 Hibernate에서 Column CamelCase -> SnakeCase 변환 안되는 문제 해결