DB에 Master-Slave 구조를 적용하고 @Transactional
어노테이션을 활용해서 @Transactional(readOnly = true)
일 경우에는 읽기 전용 DB로, @Transactional(readOnly = false)
또는 기본 옵션일 경우에는 쓰기 전용 DB로 DataSource
가 동적으로 매핑되는 기능을 구현하기 위해 아래와 같은 Configuration을 했습니다.
@Configuration
public class DataSourceConfiguration {
public static final String PRIMARY_DATASOURCE = "primaryDataSource";
public static final String SECONDARY_DATASOURCE = "secondaryDataSource";
@Bean(PRIMARY_DATASOURCE)
@ConfigurationProperties(prefix = "spring.datasource.primary.hikari")
public DataSource primaryDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean(SECONDARY_DATASOURCE)
@ConfigurationProperties(prefix = "spring.datasource.secondary.hikari")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean
@Primary
@DependsOn({PRIMARY_DATASOURCE, SECONDARY_DATASOURCE})
public DataSource routingDataSource(
@Qualifier(PRIMARY_DATASOURCE) DataSource primaryDataSource,
@Qualifier(SECONDARY_DATASOURCE) DataSource secondaryDataSource
) {
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("primary", primaryDataSource);
dataSourceMap.put("secondary", secondaryDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(primaryDataSource);
return routingDataSource;
}
@Bean
@DependsOn("routingDataSource")
public LazyConnectionDataSourceProxy dataSource(DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
? "secondary"
: "primary";
}
}
}
위 구현이 동작하기 위해 application.yml
도 아래와 같이 작성했습니다.
spring:
datasource:
primary:
hikari:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: password
jdbc-url: jdbc:mysql://localhost:33306/db-master
secondary:
hikari:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: password
jdbc-url: jdbc:mysql://localhost:33307/db-slave
위 구현으로 동적 DataSource
매핑 기능은 정상적으로 돌아갔습니다.
하지만, 테스트 코드에서는 동적 매핑 기능을 사용하지 않고 단일 DB로 테스트를 하고 싶었습니다. 그래서 테스트 코드용 application.yml
파일을 따로 두고 해당 파일에서는 아래와 같이 spring boot에서 제공하는 기본 spring.datasource.~
프로필을 설정했습니다.
spring:
datasource:
url: jdbc:h2:mem:db
driver-class-name: org.h2.Driver
username: sa
password:
여기서 설정된 프로필이 우선적으로 인식되지 않고 위에서 설정한 Configuration으로 인해 application.yml
의 datasource 프로필이 씹히는 문제가 발생했습니다.
동일한 명칭의 비어있는 Configuration 클래스를 test 패키지의 동일한 디렉토리에 생성하여 main 디렉토리의 Configuration을 무시할 수 있었습니다.
하지만, 이는 불필요한 파일의 생성이 필요하며, 빈의 생성 조건을 명시적으로 표현하는 방식이 아니기 때문에 좋은 방법이 아니라고 판단했고 최종적으로 다음과 같은 방식으로 해결했습니다.
@Configuration
public class DataSourceConfiguration {
// ...
@Bean(PRIMARY_DATASOURCE)
@ConditionalOnProperty(prefix = "spring.datasource.primary.hikari", name = "jdbc-url")
@ConfigurationProperties(prefix = "spring.datasource.primary.hikari")
public DataSource primaryDataSource() {
// ...
}
@Bean(SECONDARY_DATASOURCE)
@ConditionalOnProperty(prefix = "spring.datasource.secondary.hikari", name = "jdbc-url")
@ConfigurationProperties(prefix = "spring.datasource.secondary.hikari")
public DataSource secondaryDataSource() {
// ...
}
@Bean
@Primary
@ConditionalOnBean(name = {PRIMARY_DATASOURCE, SECONDARY_DATASOURCE})
@DependsOn({PRIMARY_DATASOURCE, SECONDARY_DATASOURCE})
public DataSource routingDataSource(
@Qualifier(PRIMARY_DATASOURCE) DataSource primaryDataSource,
@Qualifier(SECONDARY_DATASOURCE) DataSource secondaryDataSource
) {
// ...
}
@Bean
@ConditionalOnBean(name = {PRIMARY_DATASOURCE, SECONDARY_DATASOURCE})
@DependsOn("routingDataSource")
public LazyConnectionDataSourceProxy dataSource(DataSource routingDataSource) {
// ...
}
// ...
}
@ConditionalOnProperty
는 application.yml
에 정의 되어있는 속성의 존재와 값에 따라 조건부로 빈 을 생성하기 위해 사용하는 어노테이션입니다.
어노테이션 옵션으로 prefix
에 property의 앞 부분을, 그리고 name
에 마지막 키 값을 넣으면 해당 속성의 존재 유무에 따라 빈을 생성하거나 생성하지 않습니다.
해당 어노테이션으로 처음에 언급한 이슈에서 문제였던 spring.datasource.~
프로필이 정상적으로 작성되었음에도 직접 작성한 Configuration의 빈 생성이 우선 순위로 잡히고, spring.datasource.primary
와 spring.datasource.secondary
가 없어서 DataSource
를 생성하지 못했던 이슈는 해결되었습니다.
@ConditionalOnBean
은 특정 빈이 이미 생성되어있는 경우에만 해당 빈을 생성하기 위해 사용하는 어노테이션입니다.
어노테이션 옵션으로 name
에 의존할 빈 이름들을 배열 형태로 넣을 수 있습니다.
해당 어노테이션으로 routingDataSource
와 LazyConnectionDataSourceProxy
빈을 조건부 생성하도록 처리해서 테스트 환경에서는 불필요한 빈을 생성하지 않도록 하였습니다.