[ Spring Batch 5 ] 프로젝트 기본 세팅법

식빵·2024년 9월 14일
0

spring-batch-5

목록 보기
1/4

글의 목표

  • Spring boot 3 기반의 Spring batch 5 프로젝트를 생성
  • spring batch 를 위한 설정법 알아보기
  • Job 의 수동실행 및 테스트 코드 작성법

필자 개발환경:

  • IDE: IntelliJ IDEA 2024.2.1 (Ultimate Edition)
  • OS : Ubuntu 22.04 (via WSL2)
  • DB : PostgreSQL 16 (via Docker)
  • JDK : temurin 21
  • Build Tool : Maven
  • 그외: Spring Boot 3.3.3, Spring Batch Core 5.1.2



🍞 프로젝트 생성 및 설정

1. Spring Boot 프로젝트 생성

Spring boot 3 기반의 프로젝트를 생성해봅시다.
https://start.spring.io/ 에 접속해서 아래와 같은 dependencies 를 갖는
프로젝트를 하나 생성하고, 자신이 사용하는 IDE 를 통해서 여시기 바랍니다.



2. application.yaml 설정

이후 spring boot 기본 프로젝트를 열면 src/main/resources/ 경로에 있는
application.properties 파일을 아래와 같이 편집합니다.

spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.driver-class-name=org.postgresql.Driver

spring.batch.jdbc.initialize-schema=always
spring.batch.jdbc.platform=postgresql
spring.batch.job.enabled=false

yaml 사용시에는 아래처럼

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: postgres
  batch:
    jdbc:
      initialize-schema: always
      platform: postgresql
    job:
      enabled: false

각 설정의 의미

  • (spring datasource 부분은 여러분 환경에 맞게 수정하세요!)

  • spring.batch.jdbc.initialize-schema=always:

    • 스프링 배치가 내부적으로 사용하는 메타 정보 테이블의 생성여부
    • always 로 하면 처음 한번 생성, 이후 기존 생성 테이블을 계속 사용
    • never 를 지정하면, 미리 자신이 해당 스키마 테이블들을 수동으로 생성해놔야 합니다.
      해당 스키마 생성 SQL 파일들은 spring-batch-core jar 내부에 있습니다.
  • spring.batch.jdbc.platform=postgresql:
    위에서 말한 initialize-schema 설정에 의해서 메타 정보 테이블을 생성할 때
    사용하게 될 스크립트를 지정하는 옵션입니다. 옵션값은 schema-<옵션값>.sql 처럼 사용되어서 실제 테이블 생성 SQL 파일을 지정하게 됩니다. sql 파일들은 모두 spring-batch-core jar 내부에 있습니다.


  • spring.batch.job.enabled=false:
    • 하나의 Job 이 Bean 으로 등록되어 있으면 spring boot 실행시 자동으로
      Job 도 실행합니다. 이를 방지하는 설정입니다.
    • 참고로 여러 Job 이 Bean 으로 등록되어 있고, 설정값이 true 면 에러가 납니다.
    • Job 수동 실행법은 다른 목차에 설명을 기재했습니다.



3. ExecutionContextSerializer 변경

spring batch 4.x 버전을 사용하시던 분들이라면
Execution Context 의 정보가 json 형태로 저장되던 것을 기억할 겁니다.

하지만 5 버전에서는 Base64 형태로 컨텍스트 정보가 저장이 되는데요.
만약에 이전처럼 json 으로 저장하고 싶다면 아래처럼 설정해주세요.

주의: 아래와 같이 jackson 라이브러리를 미리 세팅해야 됩니다.

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

@SpringBootApplication
public class SpringBatchPlaygroundApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBatchPlaygroundApplication.class, args);
    }
    
    // **** 아래처럼 빈 하나를 등록하면 끝입니다. ****
    @Bean
    public ExecutionContextSerializer executionContextSerializer() {
        return new Jackson2ExecutionContextStringSerializer();
    }
}



4. JobRegistry 설정법 변경

목차는 spring boot 3.3.x 이하 버전을 사용할 때 나오는 WARNING 로그를
없애기 위한 설정입니다. spring boot 3.4.x 이상의 버전을 사용하시는
분들은 이 목차를 읽지말고 넘어가시면 됩니다.

Spring batch 에서 사용되는 JobRegistry 을 위해 제공되는
Spring boot 3 의 자동설정에는 현재 약간 문제가 있습니다.
이를 해결하기 위해서는 다음과 같이 2개의 Bean 을 등록하면 됩니다.

  • BeanDefinitionRegistryPostProcessor :
    문제를 일으키는 JobRegistryBeanPostProcessor 제거
  • JobRegistrySmartInitializingSingleton :
    앞서 제거한 JobRegistry 설정을 다른 방법으로 재적용

아래 코드처럼 작성하면 됩니다.

package coding.toast.batch;

import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class SpringBatchStudyApplication {
	
	public static void main(String[] args) {
		SpringApplication.run(SpringBatchStudyApplication.class, args);
	}
	
    
    // 1. jobRegistryBeanPostProcessor 비활성화
	@Bean
	public static BeanDefinitionRegistryPostProcessor jobRegistryBeanPostProcessorRemover() {
		return registry -> registry.removeBeanDefinition("jobRegistryBeanPostProcessor");
	}
    
    // 2. jobRegistryBeanPostProcessor 를 대체할 JobRegistrySmartInitializingSingleton 빈 등록
    @Bean
    public JobRegistrySmartInitializingSingleton 
    	   jobRegistrySmartInitializingSingleton(JobRegistry jobRegistry) {
        return new JobRegistrySmartInitializingSingleton(jobRegistry);
    }
}

여기서부터 조금 설명이 길어지는데, 그냥 프로젝트 설정만 빠르게 하실분들은
다음 목차로 바로 이동하시면 됩니다. 그냥 에러가 왜나는지에 대한 설명입니다.


문제 원인과 해결법 상세

먼저 jobRegistryBeanPostProcessor 를 제거하지 안 하고
프로젝트를 실행하면 아래와 같이 많은 WARN 문구가 나옵니다.

이는 BPP(BeanPostProcessor) 와 일반 Bean 의 등록 순서 문제입니다.
(이 문제가 뭔지 궁금하면 이 링크를 참고하세요)

이와 관련해서는 깃헙 이슈에도 문의 글이 한번 올라왔었죠.
그리고 해당 글에서는 이에 대한 해결법으로 문제를 일으키는 BPP 를 제거하라고 합니다.

@Bean
public static BeanDefinitionRegistryPostProcessor jobRegistryBeanPostProcessorRemover() {
    return registry ->
            registry.removeBeanDefinition("jobRegistryBeanPostProcessor");
}

그런데 어떤 분들은 이렇게 하니 기존에 JobRegistry 기능을 사용하는 부분에서
에러가 난다고 하더군요. 관련 설정을 지웠으니 어찌보면 당연합니다.
그러면 WARN 이 보여도 억지로 jobRegistryBeanPostProcessor 를 써야할까요?

다행히 JobRegistry 의 설정 방법에는 jobRegistryBeanPostProcessor 외에도
JobRegistrySmartInitializingSingleton 를 통해서도 가능합니다.
그래서 이 부분 또한 코드에 추가했던 거죠.

@Bean
public JobRegistrySmartInitializingSingleton 
	jobRegistrySmartInitializingSingleton(JobRegistry jobRegistry) {
    	return new JobRegistrySmartInitializingSingleton(jobRegistry);
}

이제 제가 위에서 말한 2개의 Bean 을 등록한 이유가 이해가 되죠?
이렇게 설정하면 WARN 로그가 나오지 않고, JobRegistry 도 평상시처럼
사용할 수 있게 됩니다.

참고로 이 부분은 spring batch 5.2 부터 고쳐질 것이라고 합니다.
하지만 현재 기준 spring boot release 버전인 3.3.3 에서는
spring batch core 버전이 5.1.2 이므로 안탑깝지만 위처럼 수동으로 문제를 해결해야 합니다.

Spring boot 3.4.x 부터 batch core 5.2.x 를 사용될 예정입니다.
https://github.com/spring-projects/spring-boot/releases/tag/v3.4.0-M3





🍞 프로젝트 정상 동작 확인

잘 설정됐는지 확인하기 위해서 Spring Boot 프로젝트를 실행해봅시다.

위처럼 로그에 WARN 로그가 없으면 일단 1차적으로 OK 입니다.


2번째로는 DBSpring Batch 가 메타정보를 저장하는 용도의 테이블들과
사용되는 시퀀스들이 생성된 것이 확인되면 성공한 겁니다.




🍞 Job 실행 및 테스트 방법

1. Job 수동 실행법

프로젝트 정상 구동은 확인으니, 이제 간단한 Job, Step 을 하나 생성하고
곧바로 구동을 시켜보겠습니다.

먼저 설정 클래스 하나 생성하고, 안에 Job, Step Bean 을 하나씩 생성해줍니다.

package coding.toast.batch.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Slf4j
@Configuration
public class MyJobConfiguration {
	
	@Bean("job-first")
	public Job job1(JobRepository jobRepository,
	                PlatformTransactionManager platformTransactionManager) {
		return new JobBuilder("job-first", jobRepository)
			.start(step1(jobRepository, platformTransactionManager))
			.build();
	}
	
	@Bean("step-first")
	public Step step1(JobRepository jobRepository,
	                  PlatformTransactionManager platformTransactionManager) {
		return new StepBuilder("step-first", jobRepository)
			.tasklet((contribution, chunkContext) -> {
				log.info("step1 executed");
				return RepeatStatus.FINISHED;
			}, platformTransactionManager)
			.build();
	}
}

해당 Job 을 수동으로 실행시키는 법은 간단합니다.
JobLauncher 와 실행시키고자 하는 Job 을 DI 받고 사용하면 끝입니다.
아래 코드 처럼 말이죠.

package coding.toast.batch;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
// ... 나머지 import 생략 ...

@SpringBootApplication
public class SpringBatchStudyApplication {
	
	public static void main(String[] args) {
		SpringApplication.run(SpringBatchStudyApplication.class, args);
	}
	
	// ... 중간 생략 ...
    
    
    // 간단한 테스트를 위해 프로젝트 실행할 때 자동으로 Job Launcher 실행시키기
	@Bean
	public CommandLineRunner commandLineRunner(JobLauncher jobLauncher,
	                                           @Qualifier("job-first") Job job) {
		return args -> {
			JobParameters jobParameters = new JobParametersBuilder().toJobParameters();
			jobLauncher.run(job, jobParameters);
		};
	}
}

주의사항

이렇게 하면 Job 을 바로 실행할 수 있습니다.
다만 여기서 알아야 할 점은 JobParameter 의 세팅을 바꾸지 않는 이상
한번 실행 성공한 Job 은 재실행되지 않는다는 점입니다.

개발시에는 여러번 Job 을 실행해서 테스트를 해야 되는 경우가 많은데,
이러한 동작 방식은 약간 걸리적 거리죠.

이런 경우에는 spring-batch-test 에서 제공하는 API 사용하는 게 좋습니다.
그 방법은 바로 다음 목차에서 보겠습니다.



2. Junit 기반 batch 테스트

프로젝트가 계속 발전하다 보면 여러개의 Job 이 생성되고,
이를 여러번 돌려보면서 테스트를 해보고 싶은 경우가 오는데요.

이때 사용하면 편한게 spring-batch-test 의 JobLauncherTestUtils 입니다.

package coding.toast.batch;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.batch.core.*;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.batch.test.context.SpringBatchTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;


@SpringBootTest
@SpringBatchTest
public class SimpleLogBatchTest {
	
	@Autowired
	private JobLauncherTestUtils jobLauncherTestUtils;
	
	@BeforeEach
	public void setup(@Qualifier("job-first") Job job1) {
		this.jobLauncherTestUtils.setJob(job1);
	}
	
	@Test
	public void testMyJob() throws Exception {
		
		// given
		// 무조건 적으로 Job 을 재실행하도록 랜덤함 JobParameter 값 하나를 추가한 JobParamBuilder 생성
		JobParametersBuilder uniqueJobParametersBuilder = this.jobLauncherTestUtils.getUniqueJobParametersBuilder();
		// 필요하다면 여기서 parameter builder 에 필요한 파라미터 정보 추가
		JobParameters jobParameters  = uniqueJobParametersBuilder.toJobParameters();
		
		// when
		JobExecution jobExecution = this.jobLauncherTestUtils.launchJob(jobParameters);
		
		// then
		Assertions.assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
	}
}
  • 참고로 getUniqueJobParametersBuilder 를 통해서 매번 유니크한 Long 타입의
    숫자값을 JobParameter 에 세팅됩니다. 편하죠?
  • 결과적으로 수동으로 Parameter 에 랜덤한 값을 줄 필요 없이 곧바로 Job 을 재실행 시킬 수 있습니다.




보충: batch db schema 분리하기

프로젝트 구현 시, batch 전용 db 스키마를 생성하고 사용하고 싶을 수 있습니다.
그럴 때는 아래처럼 하면 됩니다.

먼저 application.properties 를 아래처럼 수정합니다.

spring.batch.jdbc.initialize-schema=always
spring.batch.jdbc.platform=postgresql
spring.batch.job.enabled=false

# 시스템 전반에서 사용되는 datasource 설정값
spring.datasource.global.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.global.username=postgres
spring.datasource.global.password=postgres
spring.datasource.global.driver-class-name=org.postgresql.Driver

# 스프링 배치용 datasource 설정값
spring.datasource.batch.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=spring_batch
spring.datasource.batch.username=postgres
spring.datasource.batch.password=postgres
spring.datasource.batch.driver-class-name=org.postgresql.Driver
  • 여기서 currentSchema=spring_batch 부분이 postgres jdbc url 이 제공하는 옵션입니다. 이렇게 함으로서 dataSource 의 default 스키마를 변경할 수 있습니다.

이후 아래와 같이 @Configuration 설정 클래스를 생성해줍니다.

package me.dailycode.springbatch.datasource;

import org.springframework.boot.autoconfigure.batch.BatchDataSource;
import org.springframework.boot.autoconfigure.batch.BatchTransactionManager;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.support.JdbcTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfiguration {

    @Bean
    @ConfigurationProperties("spring.datasource.global")
    public DataSourceProperties globalDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    public DataSource dataSource() {
        return globalDataSourceProperties()
                .initializeDataSourceBuilder()
                .build();
    }

    @Bean
    @Primary
    public PlatformTransactionManager transactionManager() {
        return new JdbcTransactionManager(dataSource());
    }

    @Bean
    @ConfigurationProperties("spring.datasource.batch")
    public DataSourceProperties batchDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @BatchDataSource
    public DataSource batchDataSource() {
        return batchDataSourceProperties()
                .initializeDataSourceBuilder()
                .build();
    }

    @Bean
    @BatchTransactionManager
    public PlatformTransactionManager batchTransactionManager() {
        return new JdbcTransactionManager(batchDataSource());
    }

}

만약 default 스키마의 설정을 Configuration 클래스 코드에서 작성하고 싶다면
아래처럼 @BatchDataSource Bean 메소드 내용을 바꾸시면 됩니다.

@Bean
@BatchDataSource
public DataSource batchDataSource() {
    HikariConfig hikariConfig = new HikariConfig();
    hikariConfig.setDriverClassName(batchDataSourceProperties().getDriverClassName());
    hikariConfig.setJdbcUrl(batchDataSourceProperties().getUrl());
    hikariConfig.setUsername(batchDataSourceProperties().getUsername());
    hikariConfig.setPassword(batchDataSourceProperties().getPassword());
    hikariConfig.setSchema("spring_batch"); // default 스키마 지정!
    return new HikariDataSource(hikariConfig);
}

이게 필요한 이유는 모든 DB 관련 jdbc 구현체가 postgresql jdbc
처럼 currentSchema 옵션을 주는 게 아니기 때문입니다.




참고한 것들

profile
백엔드 개발자로 일하고 있는 식빵(🍞)입니다.

0개의 댓글