[Spring Boot] 멀티 Datasource 연결하기

왔다 정보리·2024년 5월 25일
0
post-thumbnail

멀티 Datasource를 연결하게 된 계기


MSA로 프로젝트를 진행하던 중, CQRS를 위해 CUD를 하는 DB와 Read를 위한 DB를 따로 두자는 이야기가 나왔다. 이렇게 DB를 2개 두면서 CUD를 위한 DB를 연결하는 작업과 Read를 위한 DB를 연결하는 작업을 따로 하게 되었다. 같은 Entity를 사용해도 상황에 따라 다른 DB에서 데이터를 불러와야 했어서 이걸 어떻게 적용해야 할지 고민이 됐는데, 역시 구글에 다양한 방법이 나와 있었다. 구글링 최고..! 그동안 application.yml을 통해 Datasource를 하나만 연결해서 사용해 왔는데 이렇게 여러 개를 연결하는 건 처음이라 진행한 방식을 기록하려 한다.

멀티 Datasource 연결하기


CommandDB : CUD를 위한 DB
QueryDB : Read를 위한 DB

1. Repository를 작성한다

@Repository
public interface MemberQueryRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByIdAndState(Long memberId, State state);
}

@EnableJpaRepositories는 Config 파일에서 작성하기 때문에 Repository에는 따로 작성하지 않아도 된다. 만약 @EnableJpaRepositories를 작성할 경우 Bean 중복 에러가 발생한다.

2. Entity를 작성한다

@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 이런 식으로 파일명을 다르게 작성해 주었다.

3. application.yml 파일을 작성한다

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

4. Datasource Config 파일을 작성한다

CommandDataSource.java

@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 어노테이션은 중복으로 작성할 수는 없으며, 그렇지 않으면 에러가 발생한다.

QueryDatasource.java

@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;
    }
}

4-1. Config 파일 상세 설명

@EnableJpaRepositories

@EnableJpaRepositories(
		basePackages = {
        	"com.example.lucky7postservice.src.command"
        },
        entityManagerFactoryRef = "primaryEntityManager",
        transactionManagerRef = "primaryTransactionManager"
)

basePackages에 Datasource와 연결된 Repository가 있는 경로를 입력한다. 경로는 여러 개 입력 가능하며, Repository가 있는 모든 경로를 입력하면 된다.

entityManagerFactoryRef, transactionManagerRef에는 밑에 정의한 Bean을 각각 적어주면 된다.

@ConfigurationProperties

@Bean
@ConfigurationProperties(prefix = "spring.datasource.query")
public DataSource secondDataSource() {
	return DataSourceBuilder.create().build();
}

prefix에 application.yml 파일에 정의한 Datasource의 경로를 적어준다. 여기에서 yml 파일에 정의된 Datasource의 정보를 가져온다.

EntityManagerFactory

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 변환 안되는 문제 해결

profile
왔다 정보리

0개의 댓글