⛓️ Multiple datasource – Spring에서 다중 데이터베이스 연결 설정하기

김공영·2024년 7월 24일

how-to

목록 보기
7/12
post-thumbnail

이 글은 다중 데이터베이스 연결을 하게 된 계기, 과정과 코드에 대해 다룹니다.
구현 방법을 보고싶으시다면 🧭 환경 설정 부터 읽는 것을 권장드립니다.

왜 다중 데이터베이스 연결이 필요한가

그거..... 해야해?

현재 개발 중인 프로젝트의 요구 사항에는 특이한 사항이 있었다. 바로.... OAuth를 지원하지는 않지만 사내 제품 관리 시스템에서 회원 가입 및 로그인이 가능해야 한다는 것이다. OAuth랑 뭐가 다른진 모르겠지만 그래도 해야 한다. 그래서 session 저장소인 Redis를 공유하는 방식으로 구현하려 했으나, 필요한 user info를 매번 가져오는 오버헤드가 크다는 이유로 아예 DB를 공유하는 방식으로 구현하기로 결정했다. 하지만 모든 schema를 공유할 필요는 없었기 때문에 view table을 만들어 원하는 정보만 가져와 사용했다.
그래서 결과적으로는 두 개의 데이터베이스(기존 서비스 데이터베이스, 회원 기능을 위한 데이터베이스)를 사용해야 했다. 두 데이터베이스를 모두 활용하기 위해 다중 데이터베이스 연결을 설정했다.

Spring은 다중 데이터베이스 연결을 지원하는가

Spring에서는 하나의 데이터베이스 연결 시 application.yml에 datasource 항목을 작성하는 것만으로도 연결할 수 있도록 지원한다. 다중 데이터베이스를 위해 별도로 지원하는 라이브러리는 없다. 다중 데이터베이스 연결을 위해 찾아보니 Configuration class를 사용해 각 data source에 대해 설정을 하는 방식이 일반적이었다. 그래서 reference가 많은 이 방식을 채택했다. 해당 방식의 단점은 아래와 같다.

  • Spring에서 자동으로 data source에 연결할 수 있도록 지원하는 방식을 사용하지 않기 때문에 직접 설정이 필요하다.
  • 수동으로 설정하기 때문에 에러가 발생하는 경우가 많고, 에러가 발생했을 때 원인을 분석해 대응하는 것이 어렵다. 이건 나의 멍청함으로 인해 직접 느낀 단점이다.
  • 지원하는 jdbc driver를 직접 찾아서 설정해줘야 한다.

다행히도 MySQL은 지원하는 jdbc driver가 있어 수월하게 설정할 수 있었다.

🧭 환경 설정

environment

  • Spring v3.3.1
  • Gradle

의존성 설정

별도의 의존성 설정을 추가할 필요는 없지만 단일 데이터소스 등록과 마찬가지로 MySQL을 위한 의존성 설정은 아래와 같이 되어있어야 한다.

build.gradle

dependencies {
	...
    // Mysql
    runtimeOnly 'com.mysql:mysql-connector-j'
    ...
}

application.yml

설정 시 test1, test2를 각 데이터베이스의 이름으로 정확하게 설정해야한다. 난 잘못 입력해서 view table 이름을 입력했다가 한 시간동안 헤맸다.

일반적인 data source 설정 시에는 spring.datasource.url로 url을 설정한다. 하지만 이 경우에는 Java로 Configuration을 구성해 data source를 설정하기 때문에 spring.datasource.jdbc-url로 url을 설정해야 한다.

spring:
  datasource:
    test1:
      jdbc-url: jdbc:mysql://localhost:3306/test1?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Seoul
      username: root
      password: password
      driver-class-name: com.mysql.cj.jdbc.Driver
      
    test2:
      jdbc-url: jdbc:mysql://localhost:3306/test2?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Seoul
      username: root
      password: password
      driver-class-name: com.mysql.cj.jdbc.Driver

🚀 How to use

Configuration

default로 설정할 data source의 Configuration 클래스에는 각 bean에 @Primary 어노테이션을 달아주면 된다.

아래 예시에서는 default로 설정할 데이터베이스 이름이 test1, 아닌 데이터베이스 이름을 test2라고 가정해 작성했다.

Test1DataSourceConfig.java

import com.zaxxer.hikari.HikariDataSource;
import java.util.HashMap;
import javax.sql.DataSource;

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;

@Configuration
@EnableJpaRepositories(
    basePackages = { // 해당 데이터베이스의 table들에 매핑되는 repository들 설정
        "com.project.api.account.repository",
        "com.project.api.content.repository"
    },
    entityManagerFactoryRef = "test1EntityManager",
    transactionManagerRef = "test1TransactionManager"
)
public class Test1DataSourceConfig {

    @Primary
    @Bean
    public PlatformTransactionManager test1TransactionManager() {

        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(test1EntityManager().getObject());

        return transactionManager;
    }

    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean test1EntityManager() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(testDataSource());
        em.setPackagesToScan(new String[] {
            "com.project.api.account.domain",
            "com.project.api.content.domain" });
        em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        em.setPersistenceUnitName("test1");

        HashMap<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "none");
        properties.put("hibernate.dialect", "org.hibernate.dialect.MySQLDialect");
        properties.put("hibernate.show_sql", "true");

        em.setJpaPropertyMap(properties);

        return em;
    }

    @Bean(name = "test1DataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.test1")
    public DataSource test1DataSource() {
        return DataSourceBuilder.create()
            .type(HikariDataSource.class)
            .build();
    }
}

Test2DataSourceConfig.java

import com.zaxxer.hikari.HikariDataSource;
import java.util.HashMap;
import javax.sql.DataSource;

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;

@Configuration
@EnableJpaRepositories(
    basePackages = { // 해당 데이터베이스의 table들에 매핑되는 repository들 설정
        "com.project.api.auth.repository"
    },
    entityManagerFactoryRef = "test2EntityManager",
    transactionManagerRef = "test2TransactionManager"
)
public class Test2DataSourceConfig {

    @Bean
    public PlatformTransactionManager test2TransactionManager() {

        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(test2EntityManager().getObject());

        return transactionManager;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean test2EntityManager() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(testDataSource());
        em.setPackagesToScan(new String[] {
            "com.project.api.auth.domain" });
        em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        em.setPersistenceUnitName("test2");

        HashMap<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "none");
        properties.put("hibernate.dialect", "org.hibernate.dialect.MySQLDialect");
        properties.put("hibernate.show_sql", "true");

        em.setJpaPropertyMap(properties);

        return em;
    }

    @Bean(name = "test2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.test2")
    public DataSource test2DataSource() {
        return DataSourceBuilder.create()
            .type(HikariDataSource.class)
            .build();
    }
}

Entity, Repository class 구현

entity, repository class는 다른 프로젝트와 동일하게 구현한다. 예시는 아래와 같다.

MemberEntity.java

package com.project.api.auth.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import org.hibernate.annotations.Immutable;

@Entity
@Immutable
@Table(name = "test2_auth")
@Getter
public class MemberEntity {

    @Id
    @Column(name = "member_id", nullable = false)
    private Long memberId;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "name", nullable = false)
    private String name;

    @Column(name = "password", nullable = false)
    private String password;
}

MemberRepository.java

package com.project.api.auth.repository;

import java.util.Optional;
import kr.co.beamworks.cadaimbe.api.auth.domain.MemberEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberRepository extends JpaRepository<MemberEntity, Long> {
    Optional<MemberEntity> findByEmail(String email);
}
profile
나는야 말하는 개발(감)자

0개의 댓글