이번에 위치추적모듈 프로젝트를 진행하며 정기결제 기능을 구현하기 위해 Spring Batch를 사용하였다. Spring Batch를 이용하다 보니 아래 사진과 같이 하나의 데이터베이스에 Domain Data와 Meta Data가 저장되어있게 되었다.
이렇게 Quartz와 Batch에서 사용하는 Meta Data 때문에 데이터베이스를 알아보기도 어려워졌고, Batch 서버에서 각각의 데이터에 대한 데이터베이스 커넥션 설정을 하지 못하는 문제도 발생하였다. 또한, SCDF를 이용한 Batch 실행 결과에 대한 모니터링 기능의 데이터베이스 연결 또한 분리하지 못하게 되었고, 이 또한 데이터베이스의 커넥션 부족 문제로 이어질 수 있었다.
이러한 문제를 해결하고자, 아래 사진과 같이 데이터베이스를 분리하고자 하였다.
이번 게시글에서는 Spring Batch 5에서 Meta 데이터베이스 분리하여 사용하는 방법에 대해서 기술하겠다.
Build Tool을 Gradle이 아닌, IntelliJ를 사용하는 것을 추천한다. 두 개의 데이터베이스 연결을 위해서는 직접 설정해야 할 Bean이 많은데, 가독성을 위해 @Qualifier를 사용하는 것을 추천하는데, @Qualifier를 사용하지 않으면 오류를 발생시켜주기 때문이다.
spring:
datasource:
meta:
hikari:
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://${local.meta-db.host}:${local.meta-db.port}/${local.meta-db.name}
username: ${local.meta-db.username}
password: ${local.meta-db.password}
domain:
hikari:
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://${local.domain-db.host}:${local.domain-db.port}/${local.domain-db.name}
username: ${local.domain-db.username}
password: ${local.domain-db.password}
auto-commit: false
yml 파일에 두 개의 데이터베이스 사용을 위한 설정 파일을 추가해준다.
DataSourceConfig 클래스는 각 데이터베이스에 대한 DataSource와 초기화 설정을 포함하고 있다.
@Configuration
public class DataSourceConfig {
public static final String META_DATASOURCE = "metaDataSource";
public static final String DOMAIN_DATASOURCE = "domainDataSource";
@Bean
@ConfigurationProperties(prefix = "spring.datasource.meta.hikari")
public HikariConfig metaHikariConfig() {
return new HikariConfig();
}
@Primary
@Bean(META_DATASOURCE)
public DataSource metaDataSource() {
return new LazyConnectionDataSourceProxy(new HikariDataSource(metaHikariConfig()));
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.domain.hikari")
public HikariConfig domainHikariConfig() {
return new HikariConfig();
}
@Bean(DOMAIN_DATASOURCE)
public DataSource domainDataSource() {
return new LazyConnectionDataSourceProxy(new HikariDataSource(domainHikariConfig()));
}
}
- metaHikariConfig() 및 domainHikariConfig(): 각각의 데이터베이스에 대한 HikariConfig 객체를 생성한다.
- metaDataSource() 및 domainDataSource(): 각각의 HikariConfig를 이용하여 LazyConnectionDataSourceProxy로 DataSource를 생성한다.
- metaDataSourceInitializer(): 테스트 환경에서 metaDataSource를 초기화한다.
TransactionManagerConfig 클래스는 각 데이터베이스에 대한 트랜잭션 관리자를 설정한다.
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties({JpaProperties.class, HibernateProperties.class})
public class TransactionManagerConfig {
public static final String DOMAIN_ENTITY_MANAGER_FACTORY = "domainEntityManagerFactory";
public static final String META_TRANSACTION_MANAGER = "metaTransactionManager";
public static final String DOMAIN_TRANSACTION_MANAGER = "domainTransactionManager";
private final JpaProperties jpaProperties;
private final HibernateProperties hibernateProperties;
private final ObjectProvider<Collection<DataSourcePoolMetadataProvider>> metadataProviders;
private final EntityManagerFactoryBuilder entityManagerFactoryBuilder;
@Bean(name = META_TRANSACTION_MANAGER)
public PlatformTransactionManager metaTransactionManager(@Qualifier(META_DATASOURCE) DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = DOMAIN_ENTITY_MANAGER_FACTORY)
public LocalContainerEntityManagerFactoryBean domainEntityManagerFactory(@Qualifier(DOMAIN_DATASOURCE) DataSource dataSource) {
return EntityManagerFactoryCreator.builder()
.properties(jpaProperties)
.hibernateProperties(hibernateProperties)
.metadataProviders(metadataProviders)
.entityManagerFactoryBuilder(entityManagerFactoryBuilder)
.dataSource(dataSource)
.packages("org.changppo.account.entity")
.persistenceUnit("domainUnit")
.build()
.create();
}
@Bean(name = DOMAIN_TRANSACTION_MANAGER)
public PlatformTransactionManager domainTransactionManager(@Qualifier(DOMAIN_ENTITY_MANAGER_FACTORY) LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return new JpaTransactionManager(Objects.requireNonNull(entityManagerFactory.getObject()));
}
@Configuration
@EnableJpaRepositories(
basePackages = "org.changppo.account.repository"
,entityManagerFactoryRef = DOMAIN_ENTITY_MANAGER_FACTORY
,transactionManagerRef = DOMAIN_TRANSACTION_MANAGER
)
public static class DomainJpaRepositoriesConfig{}
}
- metaTransactionManager(): meta 데이터베이스에 대한 트랜잭션 관리자 생성.
- domainEntityManagerFactory(): domain 데이터베이스에 대한 EntityManagerFactory 생성.
- domainTransactionManager(): domain 데이터베이스에 대한 트랜잭션 관리자 생성.
참고로 EntityManagerFactoryCreator는 특정 구성 설정을 통해 EntityManagerFactory를 생성하는 유틸리티 클래스이다. 이 내용에 대한 자세한 사항은 아래 게시글을 참고하면 된다.
BatchConfig 클래스는 Spring Batch의 설정을 담당한다.
@Configuration
public class BatchConfig extends DefaultBatchConfiguration {
private final DataSource mainDataSource;
private final PlatformTransactionManager metaTransactionManager;
public BatchConfig(@Qualifier(DataSourceConfig.META_DATASOURCE) DataSource metaDataSource, @Qualifier(META_TRANSACTION_MANAGER) PlatformTransactionManager metaTransactionManager) {
this.mainDataSource = metaDataSource;
this.metaTransactionManager = metaTransactionManager;
}
@Override
protected DataSource getDataSource() {
return mainDataSource;
}
@Override
protected PlatformTransactionManager getTransactionManager() {
return metaTransactionManager;
}
}
- getDataSource(): 메타 데이터베이스에 대한 DataSource 반환.
- getTransactionManager(): 메타 데이터베이스에 대한 트랜잭션 관리자 반환.
JobConfig 클래스는 Spring Batch의 Job을 정의한다.
@Configuration
@Slf4j
public class JobConfig {
public static final String AUTOMATIC_PAYMENT_JOB = "automaticPaymentExecutionJob";
public static final String AUTOMATIC_PAYMENT_STEP = "executeAutomaticPaymentStep";
private final JobRepository jobRepository;
private final PlatformTransactionManager domainTransactionManager;
public JobConfig(JobRepository jobRepository, @Qualifier(DOMAIN_TRANSACTION_MANAGER) PlatformTransactionManager domainTransactionManager) {
this.jobRepository = jobRepository;
this.domainTransactionManager = domainTransactionManager;
}
@Bean(AUTOMATIC_PAYMENT_JOB)
public Job automaticPaymentExecutionJob(@Qualifier(AUTOMATIC_PAYMENT_STEP) Step executeAutomaticPaymentStep) {
return new JobBuilder("AutomaticPaymentExecutionJob", jobRepository)
.incrementer(new RunIdIncrementer())
.start(executeAutomaticPaymentStep)
.build();
}
@Bean(AUTOMATIC_PAYMENT_STEP)
public Step executeAutomaticPaymentStep(@Qualifier(AUTOMATIC_PAYMENT_READER) QuerydslNoOffsetPagingItemReader<Member> memberItemReaderForAutomaticPayment,
@Qualifier(AUTOMATIC_PAYMENT_PROCESSOR) ItemProcessor<Member, Payment> paymentProcessorForAutomaticPayment,
@Qualifier(AUTOMATIC_PAYMENT_WRITER) ItemWriter<Payment> paymentItemWriterForAutomaticPayment) {
return new StepBuilder("executeAutomaticPaymentStep", jobRepository)
.<Member, Payment>chunk(10, domainTransactionManager)
.reader(memberItemReaderForAutomaticPayment)
.processor(paymentProcessorForAutomaticPayment)
.writer(paymentItemWriterForAutomaticPayment)
.build();
}
}
- automaticPaymentExecutionJob(): 자동 결제 Job 정의.
- executeAutomaticPaymentStep(): 자동 결제 Step 정의.
Domain
domain 데이터베이스를 분리하며 domain 데이터베이스에 대해 Api Server와 Batch Server의 데이터베이스 커넥션 풀 크기를 유동적으로 설정할 수 있다.
Meta
SCDF를 연결하면서 정말 많은 데이터베이스 테이블이 생기게 되었다. meta 데이터베이스를 분리하며 가독성도 좋아졌고, 데이터베이스를 관리하기 정말 쉬워졌다. 또한 Batch Server와 SCDF의 데이터베이스 커넥션 풀 크기를 유동적으로 설정할 수 있다.