회사에서 개발을 진행하다가 원래는 하나의 데이터베이스만 사용했는데, 이번에 새로운 기능이 추가되면서 다른 데이터베이스를 붙여야 하는 일이 있었습니다.

처음 시도해보는거라 까먹을 것 같아서 나중에 필요하면 찾아볼 겸 데이터베이스를 두개 연결해서 사용하는 방법을 정리해봅니다.

1. YML 설정

spring:
  datasource:
    db1:
      jdbc-url: jdbc:log4jdbc:mysql://<DB1_주소>
      username: username
      password: password
      driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy

    db2:
      jdbc-url: jdbc:log4jdbc:mysql://<DB2_주소>
      username: username
      password: password
      driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy

원래 dataSource가 하나였을땐 스프링이 yml파일을 읽은 뒤 빈으로 등록해주지만, 두개부터는 각 dataSource마다 이름을 만들어주어야 하기 때문에 자바 코드로 직접 설정을 해주어야 합니다.


2. Java 코드 설정

1. DataSource 설정

@Configuration
public class DataSourceConfig {

    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.db1")
    public DataSource db1DataSource() {
        return new DataSourceBuilder.create().build();
    }

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

}

@Primary 어노테이션을 붙인이유는, @Primary를 붙이지 않고 실행을 하게 되면 빈을 주입할 때 어떤걸 주입해야할지 애매하기 때문에 예외가 발생합니다.
그래서 기본으로 등록할 dataSource에 @Primary를 설정해줍니다.

@ConfigurationProperties 어노테이션의 속성에 yml파일에서 설정한 prefix를 입력하면, 설정파일을 읽어서 값을 세팅해줍니다.
정확하진 않지만 궁금해서 내부 구현을 살펴보니, 아마 이부분에 의해서 값이 세팅되는 것 같습니다.

 

2. JPA 설정

@Configuration
@EnableJpaRepositories(
        basePackages = <컴포넌트 스캔할 대상이 있는 패키지>
        , entityManagerFactoryRef = "db1EntityManagerFactory"
        , transactionManagerRef = "db1JpaTransactionManager"
)
public class Db1JpaConfig {

    @Bean
    @Primary
    public EntityManagerFactory db1EntityManagerFactory(@Qualifier("db1DataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setDataSource(dataSource);
        factory.setPackagesToScan("..."); <JPA Entity가 존재하는 패키지를 작성>
        factory.setPersistenceUnitName("db1EntityManager");
        factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    @Bean
    @Primary
    public PlatformTransactionManager db1JpaTransactionManager(@Qualifier("db1EntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(entityManagerFactory);
        return tm;
    }
@Configuration
@EnableJpaRepositories(
        basePackages = <컴포넌트 스캔할 대상이 있는 패키지>
        , entityManagerFactoryRef = "db2EntityManagerFactory"
        , transactionManagerRef = "db2JpaTransactionManager"
)
public class Db2JpaConfig {

    @Bean
    public EntityManagerFactory db2EntityManagerFactory(@Qualifier("db2DataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setDataSource(dataSource);
        factory.setPackagesToScan("..."); <JPA Entity가 존재하는 패키지를 작성>
        factory.setPersistenceUnitName("db2EntityManager");
        factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    @Bean
    public PlatformTransactionManager db2JpaTransactionManager(@Qualifier("db2EntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(entityManagerFactory);
        return tm;
    }



3. 사용방법

@Service
public class ProductService {
    @Transactional("db1JpaTransactionManager") // 생략 가능
    public void db1Products() {
        ...
    }

    @Transactional("db2JpaTransactionManager")
    public void db2Products() {
        ...
    }
}

db1JpaTransactionManager에 생략가능을 적어놓은 이유는,
JpaConfig에서 설정할 때 db1JpaTransactionManager에 @Primary를 적용했기 때문에 기본 트랜잭션 매니저가 db1JpaTransactionManager로 지정되어있어서 생략가능하다고 적어놓은 것입니다.


4. 문제

여기까지 하면 데이터베이스 두개 연결을 완료했습니다.

만약 하나의 기능에서 두개의 데이터베이스에 insert, update하는 기능이 없고, 각 기능에서 각각의 데이터베이스만 사용한다면 당분간은 큰 문제 없이 이대로 사용할 수 있을 것입니다.
하지만 그런 기능이 있다면 문제는 이때 발생합니다.

보통 데이터베이스에 접근해서 어떤 작업을 할 때 @Transactional 이용하는데, 트랜잭션을 시작하려면 커넥션이 필요한데, 이때 트랜잭션 매니저가 내부에서 dataSource를 사용해서 저장되어있는 커넥션을 꺼내옵니다.
그리고 그 커넥션을 이용해서 실제 데이터베이스에 쿼리를 보내는 등 작업을 시작하게됩니다.

근데 지금까지 작성한 코드를 보면 각 트랜잭션 매니저는 각각의 데이터소스만 가지고있고, 서로에 대한 데이터소스는 모르는 상태입니다.

@Transactional
public void db1_db2() {
    ...
}

위의 코드에서 db1_db2() 메서드를 살펴보면 @Transactional에 옵션이 지정되어있지 않아서 db1JpaTransactionManager 트랜잭션 매니저를 사용하게 될 것입니다.

db1JpaTransactionManager 트랜잭션 매니저가 알고 있는 dataSource는 db1DataSource이기 때문에 db2DataSource에 대한 작업을 시도하면 어떤 문제가 발생할지 모르고, 만약 작업 중 예외가 발생했을 때 제대로 rollback을 시켜줄 수도 없습니다.

예외가 발생했을 때 rollback이 안되는 일은 데이터 정합성에 크게 문제가 될 수 있기 때문에 트랜잭션을 보장해 줄 수 있는 방법을 찾아야 합니다.


다음 글에서는 Atomikos에서 제공하는 JtaTransactionManager를 이용해서 멀티 데이터베이스 연결하는 방법을 작성해보겠습니다.

+ Recent posts