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 이름을 지정해야 한다.