Spring Boot | 데이터베이스 접근 | 복수 데이터 소스 사용

기본

src/main/java/sample/springboot/PrimaryDataSourceConfiguration.java

package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
public class PrimaryDataSourceConfiguration {

    @Bean @Primary
    public DataSource createPrimaryDataSource() {
        return DataSourceBuilder
            .create()
            .driverClassName("org.hsqldb.jdbcDriver")
            .url("jdbc:hsqldb:mem:primary")
            .username("SA")
            .password("")
            .build();
    }

    @Bean @Primary
    public JdbcTemplate createPrimaryJdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }
}

src/main/java/sample/springboot/SecondaryDataSourceConfiguration.java

package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
public class SecondaryDataSourceConfiguration {

    @Bean @MySecondary
    public DataSource createSecondaryDataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName("org.hsqldb.jdbcDriver")
                .url("jdbc:hsqldb:mem:secondary")
                .username("SA")
                .password("")
                .build();
    }

    @Bean @MySecondary
    public JdbcTemplate createSecondaryJdbcTemplate(@MySecondary DataSource ds) {
        return new JdbcTemplate(ds);
    }
}

src/main/java/sample/springboot/MySecondary.java

package sample.springboot;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.beans.factory.annotation.Qualifier;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface MySecondary {
}
MyDatabaseAccess.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

@Component
public class MyDatabaseAccess {

    private static final String CREATE_TABLE_SQL = "CREATE TABLE TEST_TABLE (VALUE VARCHAR(256))";
    private static final String INSERT_SQL = "INSERT INTO TEST_TABLE VALUES (?)";
    private static final String SELECT_SQL = "SELECT * FROM TEST_TABLE";

    @Autowired
    private JdbcTemplate primary;

    @Autowired @MySecondary
    private JdbcTemplate secondary;

    public void initialize() {
        this.primary.execute(CREATE_TABLE_SQL);
        this.secondary.execute(CREATE_TABLE_SQL);
    }

    public void insertPrimary(String value) {
        this.primary.update(INSERT_SQL, value);
    }

    public void insertSecondary(String value) {
        this.secondary.update(INSERT_SQL, value);
    }

    public void showRecords() {
        System.out.println("Primary >>>>");
        this.primary.queryForList(SELECT_SQL).forEach(System.out::println);

        System.out.println("Secondary >>>>");
        this.secondary.queryForList(SELECT_SQL).forEach(System.out::println);
    }
}

src/main/java/sample/springboot/Main.java

package sample.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyDatabaseAccess db = ctx.getBean(MyDatabaseAccess.class);

            db.initialize();

            db.insertPrimary("primary!!");
            db.insertSecondary("secondary!!");

            db.showRecords();
        }
    }
}

동작 확인

콘솔 출력

Primary >>>>
{VALUE=primary!!}

Secondary >>>>
{VALUE=secondary!!}

설명

src/main/java/sample/springboot/PrimaryDataSourceConfiguration.java

    @Bean @Primary
    public DataSource createPrimaryDataSource() {
        return DataSourceBuilder
            .create()
            .driverClassName("org.hsqldb.jdbcDriver")
            .url("jdbc:hsqldb:mem:primary")
            .username("SA")
            .password("")
            .build();
    }

    @Bean @Primary
    public JdbcTemplate createPrimaryJdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }
  • @Bean을 사용하여 DataSource의 빈을 정의하고 있다 (createPrimaryDataSource()).
  • 만든 DataSource를 인수받게 되고, 또한 JdbcTemplate의 빈을 정의하고 있다 (createPrimaryJdbcTemplate()).
  • DataSource를 여러 정의 할 때 하나의 정의를 @Primary에서 주석한다.
    • @Primary은 기본적으로 주입되는 bean을 명시하는 어노테이션이다.
    • 빈의 후보가 복수 존재하는 상태에서 한정자를 지정하지 않으면 @Primary에서 어노테이션된 빈이 주입된다.
  • DataSource 인스턴스는 DataSourceBuilder를 사용하여 작성할 수 있다.

src/main/java/sample/springboot/SecondaryDataSourceConfiguration.java

    @Bean @MySecondary
    public DataSource createSecondaryDataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName("org.hsqldb.jdbcDriver")
                .url("jdbc:hsqldb:mem:secondary")
                .username("SA")
                .password("")
                .build();
    }

    @Bean @MySecondary
    public JdbcTemplate createSecondaryJdbcTemplate(@MySecondary DataSource ds) {
        return new JdbcTemplate(ds);
    }
  • 두 번째 DataSource의 정의는 자작 한정자를 부여하고 있다.

src/main/java/sample/springboot/MyDatabaseAccess.java

    @Autowired
    private JdbcTemplate primary;

    @Autowired @MySecondary
    private JdbcTemplate secondary;
  • 삽입할 때 @Autowired 뿐이라면 @Primary에서 어노테이션한 쪽의 빈이 자작 한정자에서 어노테이션하면 해당 빈이 인젝션(주입)된다.
  • 나머지는 대체로 지금까지한대로 하면 데이터베이스 액세스가 가능하다.

선언적인 트랜잭션

여러 DataSource를 정의한 경우, 그대로라면 @Primary 아닌 데이터 소스에 대한 선언적인 트랜잭션을 사용할 수 없다.

@Primary이 아닌 데이터 소스로 선언적인 트랜잭션을 사용하는 경우는 다음과 같이 구현한다.

코드 작성

src/main/java/sample/springboot/PrimaryDataSourceConfiguration.java

package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
+ import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+ import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class PrimaryDataSourceConfiguration {

    @Bean @Primary
    public DataSource createPrimaryDataSource() {
        return DataSourceBuilder
            .create()
            .driverClassName("org.hsqldb.jdbcDriver")
            .url("jdbc:hsqldb:mem:primary")
            .username("SA")
            .password("")
            .build();
    }

    @Bean @Primary
    public JdbcTemplate createPrimaryJdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }

+   @Bean @Primary
+   public PlatformTransactionManager createTransactionManager(DataSource ds) {
+       return new DataSourceTransactionManager(ds);
+   }
}

src/main/java/sample/springboot/SecondaryDataSourceConfiguration.java

package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
+ import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+ import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class SecondaryDataSourceConfiguration {

+   public static final String TRANSACTION_MANAGER_NAME = "secondary-tx-manager";

    @Bean @MySecondary
    public DataSource createSecondaryDataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName("org.hsqldb.jdbcDriver")
                .url("jdbc:hsqldb:mem:secondary")
                .username("SA")
                .password("")
                .build();
    }

    @Bean @MySecondary
    public JdbcTemplate createSecondaryJdbcTemplate(@MySecondary DataSource ds) {
        return new JdbcTemplate(ds);
    }

+   @Bean(name=SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
+   public PlatformTransactionManager createTransactionManager(@MySecondary DataSource ds) {
+       return new DataSourceTransactionManager(ds);
+   }
}

src/main/java/sample/springboot/MyDatabaseAccess.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
+ import org.springframework.transaction.annotation.Transactional;

@Component
public class MyDatabaseAccess {

    private static final String CREATE_TABLE_SQL = "CREATE TABLE TEST_TABLE (VALUE VARCHAR(256))";
    private static final String INSERT_SQL = "INSERT INTO TEST_TABLE VALUES (?)";
    private static final String SELECT_SQL = "SELECT * FROM TEST_TABLE";

    @Autowired
    private JdbcTemplate primary;

    @Autowired @MySecondary
    private JdbcTemplate secondary;

    public void initialize() {
        this.primary.execute(CREATE_TABLE_SQL);
        this.secondary.execute(CREATE_TABLE_SQL);
    }

-   public void insertPrimary(String value) {
-       this.primary.update(INSERT_SQL, value);
-   }
-   
-   public void insertSecondary(String value) {
-       this.secondary.update(INSERT_SQL, value);
-   }

+   @Transactional
+   public void insertPrimary(String value, boolean rollback) {
+       this.primary.update(INSERT_SQL, value);
+       if (rollback) throw new RuntimeException("test exception");
+   }
+   
+   @Transactional(SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
+   public void insertSecondary(String value, boolean rollback) {
+       this.secondary.update(INSERT_SQL, value);
+       if (rollback) throw new RuntimeException("test exception");
+   }

    public void showRecords() {
        System.out.println("Primary >>>>");
        this.primary.queryForList(SELECT_SQL).forEach(System.out::println);

        System.out.println("Secondary >>>>");
        this.secondary.queryForList(SELECT_SQL).forEach(System.out::println);
    }

}

src/main/java/sample/springboot/Main.java

package sample.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyDatabaseAccess db = ctx.getBean(MyDatabaseAccess.class);

            db.initialize();

            db.insertPrimary("primary commit!!", false);
            db.insertSecondary("secondary commit!!", false);

            try {
                db.insertPrimary("primary rollback!!", true);
            } catch (Exception e) {}

            try {
                db.insertSecondary("secondary rollback!!", true);
            } catch (Exception e) {}

            db.showRecords();
        }
    }
}

기동확인

콘솔 출력

Primary >>>>
{VALUE=primary commit!!}

Secondary >>>>
{VALUE=secondary commit!!}

설명

src/main/java/sample/springboot/PrimaryDataSourceConfiguration.java

    @Bean @Primary
    public PlatformTransactionManager createTransactionManager(DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }

src/main/java/sample/springboot/SecondaryDataSourceConfiguration.java

    public static final String TRANSACTION_MANAGER_NAME = "secondary-tx-manager";

    ...

    @Bean(name=SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
    public PlatformTransactionManager createTransactionManager(@MySecondary DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
  • 여러 데이터 소스를 정의한 후에 선언적 트랜잭션을 사용하는 경우PlatformTransactionManager의 bean을 정의한다.
  • @Primary 쪽은 @Primary에서 어노테이션만으로 괜찮지만, 그렇지 않은 쪽은 bean 이름을 지정 둔다.

src/main/java/sample/springboot/MyDatabaseAccess.java

    @Transactional
    public void insertPrimary(String value, boolean rollback) {
        this.primary.update(INSERT_SQL, value);
        if (rollback) throw new RuntimeException("test exception");
    }
    @Transactional(SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
    public void insertSecondary(String value, boolean rollback) {
        this.secondary.update(INSERT_SQL, value);
        if (rollback) throw new RuntimeException("test exception");
    }
  • @Primary의 DataSource를 사용하는 경우에는 @Transactional 어노테이션이 부여함으로써 선언적 트랜잭션이 사용할 수있다.
  • @Primary가 아닌 DataSource를 사용하는 경우는 @Transactional의 value에 PlatformTransactionManager의 bean 이름을 지정해야 한다.

참고