-
Spring Batch에서 멀티 소스 연결해 사용하기 (Oracle, MySQL)Projects/CDC_Oracle_to_MySQL 2025. 2. 28. 22:39
Spring Batch에서 두개의 DB를 연결해 데이터를 옮기는 작업을 해야했습니다.
그래서 멀티소스 연결이 필요했는데, 그 과정에서 수많은 에러를 겪었습니다.
에러를 해결하는 과정과 함께 Spring Batch에서 멀티소스를 연결하는 방법을 기록하고자 합니다.
먼저, 에러를 겪는 과정에서 놓친 점에 대해 짚고 넘어가겠습니다.
1. 모든 Bean은 기본 Bean이 명확해야한다
2. Spring Batch의 JobRepository에서 사용하는 DataSource와 내부 서비스 로직에서 사용하는 DB와는 무관하다
3. 기본 Bean이면 @Quarifier가 필요 없고, 기본 빈이 아니면 필요하다.
모든 Bean은 '기본' Bean이 명확해야한다
그동안은 멀티소스가 아니라 항상 하나의 DB만 연결이 필요했기 때문에 yml을 설정해준 것 말고는 모든 빈등록을 Spring Boot의 자동 설정 기능이 해줘서 DB가 연결될 때 필요한 빈과, 해당 빈들이 등록되는 과정을 몰랐습니다.
처음에는 Spring Boot의 자동설정 방식을 이해하지 못한 채로 멀티소스를 연결하려니까, @Primary와 @Qurifier를 잘못 사용하고, Bean을 잘못 생성했었습니다.
이 과정에서 가장 크게 놓친점은 모든 Bean은 기본 Bean이 반드시 존재하며 하나만 있어야 한다는 점이였습니다.
Spring Boot는 자동 설정 기능이 활성화가 되면, 자동 설정할 클래스들을 로드합니다.
이후 빈을 생성할 때, @ConditionalOnMissingBean 어노테이션에 의해서, 기본 빈이 있으면 생성하고, 없으면 생성하지 않습니다.
이때 Bean의 이름과 @Primary 어노테이션의 유무가 중요합니다.
모든 Bean 이름은 클래스 명을 따라갑니다. 예를들어 클래스명이 MyDataSource일 경우, "myDataSource"가 빈 이름이 됩니다. (name 속성으로 빈 이름을 명시할 수 있습니다.)
또한 모든 기본 Bean 이름은 클래스의 타입 명을 따라갑니다. 예를들어, DataSource에 대한 기본 빈 이름은 "dataSource"입니다.
Spring Boot는 @Primary가 붙거나, 기본 빈 이름을 가진 빈을 기본 빈으로 인식합니다.
예를들어, DataSource 빈을 자동 생성하려고 할 때,
개발자가 수동 생성하지 않음
→ Bean 자동 생성
개발자가 수동생성함 + @Primary 없음 + 빈 이름 "myDataSource"
→ 기본 Bean("dataSource")자동 생성 + 수동 빈("myDataSource") 추가 생성
개발자가 따로 수동생성함, @Primary 있음 + 빈 이름 "myDataSource"
→ 수동 빈("myDataSource")만 생성. 하지만 "dataSource"라는 빈이 안만들어졌기 때문에 내부적으로 기본 빈이 없다고 인식
→ "dataSource"를 찾을 수 없다는 에러 발생
개발자가 따로 수동생성함, @Primary 있음 + 빈 이름 "dataSource"
→ 수동 빈("dataSourcr")만 생성되며, 해당 빈을 기본 빈으로 인식
개발자가 따로 수동생성함, @Primary 없음 + 빈 이름 "dataSource"
→ 수동 빈("dataSourcr")만 생성되며, 해당 빈을 기본 빈으로 인식
또한, DataSource 빈을 여러개 만들 때, @Primary가 여러개이거나, "dataSource" 이름의 빈이 여러개이면, 해당 빈을 사용할 때 어떤 것이 기본 빈인지 판단할 수 없어 에러가 납니다.
Spring Batch 실행에 필요한 DB와 내부 서비스 로직에서 사용하는 DB와는 무관
코드를 이곳 저곳 수정하다가, JobRepository에 연결된 DB가 내부 로직에 영향을 끼치는건 아닌가 해서 애꿎은 JobRepository만 수정을 했었습니다.
그동안은 하나의 DB로 메타데이터도 연결하고, 서비스 로직도 해당 DB에 연결하다 보니 둘이 뭔가 관련이 있다고 생각했지만, Batch 실행에 필요한 DB는 오로지 실행에 있어서 메타데이터를 관리하기 위한 DB입니다.
따라서 Batch 실행에 필요한 DB와, 내부 서비스 로직에서 사용하는 DB는 완전히 달라도됩니다.
기본 Bean이면 @Quarifier가 필요 없고, 기본 빈이 아니면 필요하다.
예를들어 "dataSource", "myDataSource" 두개의 빈을 생성했을 때,
기본 DataSource인 'dataSource'를 사용하고 싶다면 그냥 DataSource dataSource를 주입받아 사용하면 됩니다.
하지만 기본 DataSource가 아닌 "myDataSource"를 사용하고 싶다면 @Quarifier를 사용해 주입받아야합니다.
// jobRepository를 수동생성할 때,기본 DataSource가 필요한 경우 @Bean @Primary public JobRepository myjJbRepository1(DataSource dataSource, PlatformTransactionManager transactionManager) throws Exception { JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean(); factory.setDataSource(dataSource); factory.setTransactionManager(transactionManager); factory.setDatabaseType("ORACLE"); factory.afterPropertiesSet(); return factory.getObject(); }// jobRepository를 수동생성할 때,기본이 아닌 DataSource가 필요한 경우 @Bean @Primary public JobRepository myjJbRepository2(@Qualifier("myDataSource") DataSource dataSource, @Qualifier("myTransactionManager") PlatformTransactionManager transactionManager) throws Exception { JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean(); factory.setDataSource(dataSource); factory.setTransactionManager(transactionManager); factory.setDatabaseType("ORACLE"); factory.afterPropertiesSet(); return factory.getObject(); }Spring Boot에서 멀티 소스 연결하기
그럼 이제 본격적으로 멀티 소스를 연결해 볼 것입니다.
제가 구현한 서비스에서는 크게 세가지 로직이 있었고, 각각 다른 DB와 DB connection을 요구했습니다.
로직A : Oracle + JDBC Template
로직B : Oracle + JPA
로직C : MySQL + JPA
해당 로직에 맞게 먼저 멀티 소스 설정을 해보겠습니다.
멀티 소스 설정
1. JPA를 위한 entity와 repository db별로 생성
DB마다 다른 엔티티매니저를 사용해 관리하도록 해야하고, 이때 관리 경로를 지정해줘야 하기 때문에 mysql과 oracle을 구분해 entity와 repository를 생성합니다.
Repository는 빈으로 등록되어야 하는데, oracle과 mysql에 둘 다 repository가 있기 때문에 반드시 기본 빈을 명시해줘야합니다.
예를들어 Oracle을 기준으로 기본빈으로 등록할 경우, oracle 패키지에 있는 repository에 모두 @Primary를 붙여줍니다.
또한, 빈 이름이 겹치치 않게 하기 위해 기본 빈이 아닌 mysql의 repository에는 빈 이름을 따로 명시해주겠습니다.


// oracle 패키지에 있는 repository를 @Primary로 기본 빈임을 명시 @Repository @Primary public interface CommentRepository extends JpaRepository<Comments,Long> { }// 기본 빈과 겹치지 않게 하기 위해 빈 이름을 명시 @Repository("mysqlCommentRepository") public interface CommentRepository extends JpaRepository<Comments,Long> { }// 기본 빈이 아닌 빈을 사용하는 예시 @RequiredArgsConstructor @Component @Import(MysqlConfig.class) //🚨어디서 관리하는 레포지토리인지를 알려주기 위해 해당 정보를 담은 MysqlConfig 빈 강제 import🚨 public class Example { @Qualifier("mysqlCommentRepository") // 기본 빈이 아니므로 어떤 빈을 사용하는지 명시 private final CommentRepository commentRepository; // : // (생략) // : }2. OracleConfig 작성
@Configuration @EnableJpaRepositories( // 관리할 레포지토리 경로 명시 basePackages = "com.example.spring_batch_1.repository.oracle", //레포지토리가 있는 패키지 경로 entityManagerFactoryRef = "entityManagerFactory", transactionManagerRef = "transactionManager" ) public class OracleConfig { @Bean(name = "dataSource") // 빈 이름을 name 속성을 이용해 지정. @Primary // 빈 이름이 기본 이름이기 때문에 없어도 되지만, 확실히 하기 위해 기본 빈으로 명시 public DataSource oracleDataSource() { return DataSourceBuilder.create() .driverClassName("oracle.jdbc.OracleDriver") .url("jdbc:oracle:thin:@111.111.111.111:1234:XE") .username("username") .password("password") .build(); } // 로직A : Oracle JDBC Template 생성 @Bean(name = "jdbcTemplate") @Primary public JdbcTemplate oracleJdbcTemplate() { return new JdbcTemplate(oracleDataSource()); //oracleDataSource를 사용 } // 로직B : Oracle JPA에서 사용할 엔티티 매니저 팩토리 생성 @Bean(name = "entityManagerFactory") @Primary public LocalContainerEntityManagerFactoryBean oracleEntityManagerFactory() { LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); factoryBean.setDataSource(oracleDataSource()); // oracleDataSource 사용 factoryBean.setPackagesToScan("com.example.cdc_spring_batch_1.entity.oracle"); //oracle Entity가 있는 패키지 경로 HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); vendorAdapter.setDatabasePlatform("org.hibernate.dialect.OracleDialect"); factoryBean.setJpaVendorAdapter(vendorAdapter); return factoryBean; } // 로직B : Oracle JPA에서 사용할 트랜잭션 매니저 생성 @Bean(name = "transactionManager") @Primary public PlatformTransactionManager transactionManager() {//JPA를 사용한 ORACLE 연결용 트랜잭션 매니저 return new JpaTransactionManager(oracleEntityManagerFactory().getObject()); } }3. MysqlConfig 작성
@Configuration @EnableJpaRepositories( basePackages = "com.example.cdc_spring_batch_1.repository.mysql", entityManagerFactoryRef = "mysqlEntityManagerFactory", transactionManagerRef = "mysqlTransactionManager" ) public class MysqlConfig { @Bean public DataSource mysqlDataSource() { return DataSourceBuilder.create() .url("jdbc:mysql://localhost:3306/db") .username("root") .password("password") .driverClassName("com.mysql.cj.jdbc.Driver") .build(); } // 로직C : MySQL JPA에서 사용할 엔티티 매니저 팩토리 생성 @Bean public LocalContainerEntityManagerFactoryBean mysqlEntityManagerFactory() { LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); factoryBean.setDataSource(mysqlDataSource()); factoryBean.setPackagesToScan("com.example.cdc_spring_batch_1.entity.mysql"); HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); vendorAdapter.setDatabasePlatform("org.hibernate.dialect.MySQL8Dialect"); // Dialect 명시적으로 설정 factoryBean.setJpaVendorAdapter(vendorAdapter); return factoryBean; } // 로직C : MySQL JPA에서 사용할 트랜잭션 매니저 생성 @Bean public PlatformTransactionManager mysqlTransactionManager() {//JPA를 사용한 MySQL 연결용 트랜잭션 매니저 return new JpaTransactionManager(mysqlEntityManagerFactory().getObject()); } }참고로, DataSource를 설정할 때 직접 url, username 등을 넣어주었기 때문에 yml파일에는 따로 datasource 설정 정보를 추가하지 않았습니다.
Spring Batch 실행에 필요한 설정
이제 멀티소스를 연결했으니, Spring Batch에 필요한 설정을 해주어야겠지만, 기본 DataSource(oracleDataSource)를 명시해줬기 때문에 Spring Boot의 자동 설정에 의해 해당 DataSource를 이용해 JobRepository, JobLauncher 등 Batch 실행에 필요한 빈들이 자동으로 만들어질 것입니다.
Spring Batch 로직 구현
@Configuration // OracleConfig과 BatchTaskletConfig에 있는 빈들이 필요하므로 강제 import @Import({OracleConfig.class, BatchTaskletConfig.class}) public class BatchStepConfig { @Bean public Step stepA(PlatformTransactionManager transactionManager, JobRepository jobRepository, Tasklet taskletA) { return new StepBuilder("StepA", jobRepository) .tasklet(taskletA, transactionManager) .allowStartIfComplete(true) .build(); } @Bean public Step stepB(PlatformTransactionManager transactionManager, JobRepository jobRepository, Tasklet taskletB) { return new StepBuilder("StepB", jobRepository) .tasklet(taskletB, transactionManager) .allowStartIfComplete(true) .build(); } @Bean public Step stepC(JobRepository jobRepository,PlatformTransactionManager transactionManager, Tasklet taskletC) { return new StepBuilder("StepC", jobRepository) .tasklet(taskletC, transactionManager) .allowStartIfComplete(true) .build(); } }@Configuration // BatchStepConfig에 있는 빈들이 필요하므로 강제 import @Import({BatchStepConfig.class}) public class BatchJob { @Bean public Job job(Step stepA, Step stepB, Step stepC, JobRepository jobRepository) { return new JobBuilder("Job", jobRepository) .start(stepA) .next(stepB) .next(stepC) .build(); } }@Configuration @Import({Consumer.class, ChangeLogConsumer.class, OracleConfig.class}) public class BatchTaskletConfig { @Bean public Tasklet taskletA(JdbcTemplate jdbcTemplate) { return ((contribution, chunkContext) -> { // : //(oracle JDBC Template를 이용한 로직A) // : }); } @Bean public Tasklet taskletB() { return ((contribution, chunkContext) -> { // : //(oracle JPA를 이용한 로직B : // Optional<Comments> optional = commentsRepository.findByRowId(rowId);) // 🚨주의🚨 oracle 패키지에 있는 repository와 entity를 import 해야합니다! // : }); } @Bean public Tasklet taskletC() { return ((contribution, chunkContext) -> { // : //(MySQL JPA를 이용한 로직C : // Optional<Comments> optional = commentsRepository.findByRowId(rowId);) // 🚨주의🚨 mysql 패키지에 있는 repository와 entity를 import 해야합니다! // : }); } }추가로 소소하게 알게된 점
1. Batch 5.x 이후 @EnableBatchProcessing 어노테이션 사라짐
아래 오류가 계속 나서 보니, @EnableBatchProcessing 어노테이션을 붙여서 그런 것이었고, 없애니까 잘 실행됨
No Micrometer observation registry found, defaulting to ObservationRegistry.NOOP2. batch 5.x 부터는 SimpleJobLauncher대신 TaskExecutorJobLauncher사용
마무리
오류 해결 과정에서 가장 도움이 됐던 것은 Spring Boot의 자동 설정 방식을 다시 제대로 공부한 것이였습니다. (역시 기본이 중요....)
이때 공부한 자동 설정 방식에 관한 글은 아래에 있습니다
https://shinebyul.tistory.com/98
Spring JPA가 Auto Configuration(자동 설정) 되는 과정 + Connenction, CP
JPA 연결 과정Data를 연결하고 관리하기 위해서는 원래 다음과 같은 과정들을 거쳐야 합니다. 1. yml 설정datasource를 연결하기 위한 설정 정보를 yml로 작성합니다. 2. DataSource 생성JPA가 DB와 연결되
shinebyul.tistory.com