이클립스 스프링 배치 프로젝트 생성 및 스케줄링 실행 방법

이클립스 STS 사용법

STS는 이클립스에서 스프링 부트 프로젝트를 쉽게 관리하고 실행할 수 있게 도와주는 플러그인입니다.
독립형 STS 이클립스를 설치해도 되고, 기존 이클립스에 STS 플러그인을 설치해도 됩니다.

독립형 STS 이클립스 설치 방법

https://spring.io/tools
Spring Tools for Eclipse - WINDOWS X86_64 선택하여 다운로드 후 압축 해제합니다.
SpringToolSuite4.exe 파일을 실행하면 STS 이클립스가 실행됩니다.
실행 시 프로젝트 workspace를 지정해야 합니다.

기존 Spring Batch 프로젝트 import 방법

프로젝트 workspace로 기존 Spring Batch 프로젝트 이동 > File > Import > Maven > Existing Maven Projects (Gradle 프로젝트는 Gradle > Existing Gradle Project) > Root Directory : Browse… > 프로젝트 폴더 선택 > Finish


이클립스 스프링배치 사용법

스프링배치 프로젝트 생성 방법

이클립스 > File > Other… > Spring Boot > Spring Stater Project >

Service URL https://start.spring.io (그대로 두기)
Name 프로젝트명-batch
Type Maven, Gradle 중 원하는 Build Tool 선택
Packaging 스프링 부트 내장 톰캣으로 실행 예정 : jar 선택
또는
외장 톰캣에 올릴 경우 : war 선택
Java Versiion JDK 버전 선택
Group 도메인에서 www 제외하고 거꾸로 작성
(예시 : www.geniatutor.co.kr = kr.co.geniatutor)
Artifact 보통 프로젝트 이름과 동일하게 설정
Description 프로젝트 설명 작성
Package Group ID 기반으로 프로젝트별 하위 패키지 추가하여 구분
(예시 : 배치 프로젝트 = kr.co.geniatutor.batch)

위 참고하여 입력 > Next > Dependencies 창에서 I/O : Spring Batch 선택 > Finish


스프링배치 프로젝트 DB 사용법

DB 정보 설정

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://DB서버IP:3306/DB명
spring.datasource.username=유저명
spring.datasource.password=패스워드

mybatis.mapper-locations=classpath:mapper/**/*.xml

application.properties 파일에 DB 정보를 작성하고, Mapper 위치를 설정합니다.

개발/운영 환경별 프로파일 설정
개발 설정 파일 : application.properties 또는 application-dev.properties
운영 설정 파일 : application-prod.properties

JDBC 드라이버 의존성 추가

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>

Maven을 사용하는 경우, pom.xml dependencies 안에 위와 같이 추가합니다.
프로젝트에서 연결할 DB에 맞는 의존성을 추가해야 합니다.
spring-boot-starter에서 의존성 버전을 관리하는 BOM을 사용하므로, 버전을 명시하지 않아도 됩니다.

MyBatis 의존성 추가

<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.2.0</version>
</dependency>

MyBatis 의존성을 추가해야 Mapper.java에 @Mapper 어노테이션을 달 수 있습니다.

메타데이터 테이블 사용 설정

spring.batch.jdbc.initialize-schema=always

application.properties 파일에 스프링배치 메타데이터 테이블 사용 설정을 합니다.
개발 단계에서는 항상 새 테이블 생성하는 always로 테스트 합니다.
운영에서는 초기 1회만 always로 실행하여 테이블을 만들고, 테이블을 생성하지 않는 never로 고정하여 사용합니다.

메타데이터 테이블 미생성 시 실행 에러메세지

java.lang.IllegalStateException: Failed to execute ApplicationRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:785) ~[spring-boot-2.5.4.jar:2.5.4]
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:772) ~[spring-boot-2.5.4.jar:2.5.4]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:345) ~[spring-boot-2.5.4.jar:2.5.4]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-2.5.4.jar:2.5.4]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1332) ~[spring-boot-2.5.4.jar:2.5.4]
	at kr.co.geniatutor.batch.GeniaBatchApplication.main(GeniaBatchApplication.java:14) ~[classes/:na]
Caused by: org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [SELECT JOB_INSTANCE_ID, JOB_NAME from BATCH_JOB_INSTANCE where JOB_NAME = ? and JOB_KEY = ?]; nested exception is java.sql.SQLSyntaxErrorException: Table 'genia_db.batch_job_instance' doesn't exist
	at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:239) ~[spring-jdbc-5.3.9.jar:5.3.9]
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:70) ~[spring-jdbc-5.3.9.jar:5.3.9]
	at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1541) ~[spring-jdbc-5.3.9.jar:5.3.9]
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:667) ~[spring-jdbc-5.3.9.jar:5.3.9]
	at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:713) ~[spring-jdbc-5.3.9.jar:5.3.9]
	at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:744) ~[spring-jdbc-5.3.9.jar:5.3.9]
	at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:757) ~[spring-jdbc-5.3.9.jar:5.3.9]
	at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:815) ~[spring-jdbc-5.3.9.jar:5.3.9]
	at org.springframework.batch.core.repository.dao.JdbcJobInstanceDao.getJobInstance(JdbcJobInstanceDao.java:151) ~[spring-batch-core-4.3.3.jar:4.3.3]
	at org.springframework.batch.core.repository.support.SimpleJobRepository.isJobInstanceExists(SimpleJobRepository.java:93) ~[spring-batch-core-4.3.3.jar:4.3.3]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) ~[spring-aop-5.3.9.jar:5.3.9]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) ~[spring-aop-5.3.9.jar:5.3.9]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.9.jar:5.3.9]
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.9.jar:5.3.9]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.9.jar:5.3.9]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.9.jar:5.3.9]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.9.jar:5.3.9]
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.9.jar:5.3.9]
	at jdk.proxy2/jdk.proxy2.$Proxy46.isJobInstanceExists(Unknown Source) ~[na:na]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) ~[spring-aop-5.3.9.jar:5.3.9]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) ~[spring-aop-5.3.9.jar:5.3.9]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.9.jar:5.3.9]
	at org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration$PassthruAdvice.invoke(SimpleBatchConfiguration.java:128) ~[spring-batch-core-4.3.3.jar:4.3.3]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.9.jar:5.3.9]
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.9.jar:5.3.9]
	at jdk.proxy2/jdk.proxy2.$Proxy46.isJobInstanceExists(Unknown Source) ~[na:na]
	at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.getNextJobParameters(JobLauncherApplicationRunner.java:206) ~[spring-boot-autoconfigure-2.5.4.jar:2.5.4]
	at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.execute(JobLauncherApplicationRunner.java:198) ~[spring-boot-autoconfigure-2.5.4.jar:2.5.4]
	at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.executeLocalJobs(JobLauncherApplicationRunner.java:173) ~[spring-boot-autoconfigure-2.5.4.jar:2.5.4]
	at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.launchJobFromProperties(JobLauncherApplicationRunner.java:160) ~[spring-boot-autoconfigure-2.5.4.jar:2.5.4]
	at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.run(JobLauncherApplicationRunner.java:155) ~[spring-boot-autoconfigure-2.5.4.jar:2.5.4]
	at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.run(JobLauncherApplicationRunner.java:150) ~[spring-boot-autoconfigure-2.5.4.jar:2.5.4]
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:782) ~[spring-boot-2.5.4.jar:2.5.4]
	... 5 common frames omitted
Caused by: java.sql.SQLSyntaxErrorException: Table 'genia_db.batch_job_instance' doesn't exist
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120) ~[mysql-connector-java-8.0.26.jar:8.0.26]
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.26.jar:8.0.26]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953) ~[mysql-connector-java-8.0.26.jar:8.0.26]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeQuery(ClientPreparedStatement.java:1003) ~[mysql-connector-java-8.0.26.jar:8.0.26]
	at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52) ~[HikariCP-4.0.3.jar:na]
	at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java) ~[HikariCP-4.0.3.jar:na]
	at org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:722) ~[spring-jdbc-5.3.9.jar:5.3.9]
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:651) ~[spring-jdbc-5.3.9.jar:5.3.9]
	... 38 common frames omitted

배치 및 DB 로그 출력

logging.level.org.springframework.batch=DEBUG
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

application.properties에 배치 및 SQL 로그를 출력하도록 설정합니다.


스프링배치 프로젝트 설정 방법

내장 톰캣 실행 포트 설정

src > main > resources > application.properties 파일에 서버 포트를 추가합니다.

server.port=8081

server.port를 설정하지 않으면, 기본 포트는 8080입니다.
실행 시 다른 톰캣 서비스와 충돌하지 않도록 포트를 변경해주는 것이 좋습니다.

프로젝트 JDK 변경

스프링 배치 프로젝트 우클릭 > Properties > Java Build Path > Libraries > Modulepath > JRE System Library 선택 > Edit… > Workspace default JRE 선택 > Finish > Apply and Close

프로젝트 내 pom.xml 또는 build.gradle에서도 java.version을 변경해줘야 합니다.

pom.xml 수정 반영 방법
프로젝트 우클릭 > Maven > Update Project

Java 버전 11로 다운그레이드 시

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.5.4</version>
  <relativePath/>
</parent>

pom.xml에서 Spring Boot 버전을 Java 11과 호환되는 2.5.x 대로 변경해야 합니다.


스프링배치 실행 및 빌드 방법

독립형 STS 이클립스에서 스프링배치 실행 방법

Boot Dashboard 탭 > local > 스프링배치 프로젝트 선택 > Start or Restart 버튼 클릭
스프링 배치는 정의된 모든 배치 스텝 처리 후 JobExecution 상태가 완료로 변경되면 자동 종료됩니다.
cron 등으로 주기적 실행을 설정한 경우는 애플리케이션이 종료되지 않고 계속 실행됩니다.

프로파일 변경 실행
Boot Dashboard 탭 > local > 스프링배치 프로젝트 우클릭 > Open Config > Profile : 변경할 프로파일 선택 > Apply > Debug

기존 이클립스에서 스프링배치 실행 방법

스프링배치 프로젝트 우클릭 > Run As > Spring Boot App

스프링배치 jar 빌드 방법

Run > Run Congigurations… > Maven Build 우클릭 > New configuration > Name : 프로젝트명 입력 > workspace… : 빌드할 프로젝트 선택 > Goals : clean package 입력 > JRE 탭 : Alternate JRE 선택 > Apply > Run
이후 재빌드 시에는 Run > Run Congigurations… > Maven Build 목록에서 프로젝트명 더블클릭 하면 됩니다.

서버 구분별 Goals 예시

  • 개발서버용 Goals : clean package -D spring.profiles.active=dev
  • 운영서버용 Goals : clean package -D spring.profiles.active=prod -D skipTests 빌드 시 프로파일에 맞는 application.properties 로드 후 TEST 코드 실행이 성공하면 패키징이 진행됩니다.
    -D skipTests 옵션을 추가하면 DB 연결 및 테스트 코드 실행 없이 바로 패키징 됩니다.

리눅스 스프링배치 jar 실행 명령어

# 개발 서버 실행
java -jar 프로젝트명.jar
또는
java -jar -Dspring.profiles.active=dev 프로젝트명.jar

# 운영 서버 실행
java -jar -Dspring.profiles.active=prod 프로젝트명.jar

프로파일 옵션 미설정 시, 실행된 프로젝트에 application.properties가 기본 적용됩니다.
프로파일 옵션 설정 시, application-프로파일명.properties가 적용됩니다.


스프링배치 서비스 등록 방법

리눅스 서버 재기동 시 스프링배치 자동 실행 가능하도록, 시스템 서비스 등록하는 방법입니다.

서비스 파일 생성

sudo vi /etc/systemd/system/프로젝트명-batch.service

아래와 같이 내용을 작성합니다.

[Unit]
Description=Spring Batch Job
After=network.target

[Service]
User=유저명
WorkingDirectory=/opt/프로젝트명
ExecStart=/java경로/java -jar -Dspring.profiles.active=프로파일명 /opt/프로젝트명/프로젝트명.jar
SuccessExitStatus=143
Restart=always

[Install]
WantedBy=multi-user.target

파일질라로 리눅스 /opt/프로젝트명 폴더에 스프링배치 jar 파일 위치시키고 서비스 파일을 생성합니다.

리눅스 java 경로 확인 명령어

which java

서비스 등록 및 자동실행 설정

# 서비스 등록
sudo systemctl daemon-reload

# 서비스 시작
sudo systemctl start 프로젝트명-batch

# 서버 재부팅 시 자동 시작 설정
sudo systemctl enable 프로젝트명-batch

스프링배치 개발 예시

매일 0시, 노출 기한이 지난 배너의 노출 여부를 비노출로 변경하는 스프링배치 예시입니다.

배너 비노출 쿼리 작성

src/main/resources/mapper/banner/BannerMapper.xml 파일을 작성합니다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.co.geniatutor.batch.banner.mapper.BannerMapper">
  <update id="disableExpiredBanners">
    UPDATE
      배너테이블명
    SET
      SHOW_YN = 'N'
    WHERE
      SHOW_YN = 'Y'
    AND
      DEL_YN = 'N'
    AND	
      SHOW_END_DT <![CDATA[<]]> NOW()
  </update>
</mapper>

배너 노출 기한이 지난 배너의 노출여부를 비노출로 변경하는 쿼리 예시입니다.
XML 파일 내에서 부등호를 사용하기 위해 CDATA를 사용하였습니다.

쿼리 Mapper 생성

src/main/java/kr/co/geniatutor/batch/banner/mapper/BannerMapper.java 파일을 작성합니다.

package kr.co.geniatutor.batch.banner.mapper;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface BannerMapper {
    void disableExpiredBanners();
}

Job 작성

src/main/java/kr/co/geniatutor/batch/banner/job/BannerBatchJobConfig.java 파일을 작성합니다.

package kr.co.geniatutor.batch.banner.job;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import kr.co.geniatutor.batch.banner.mapper.BannerMapper;

@Configuration
public class BannerBatchJobConfig {

  @Autowired
  private JobBuilderFactory jobBuilderFactory;

  @Autowired
  private StepBuilderFactory stepBuilderFactory;

  @Autowired
  private BannerMapper bannerMapper;

  @Bean
  public Job bannerJob() {
    return jobBuilderFactory.get("bannerJob")
            .incrementer(new RunIdIncrementer())
            .start(bannerStep())
            .build();
  }

  @Bean
  public Step bannerStep() {
    return stepBuilderFactory.get("bannerStep")
            .tasklet((contribution, chunkContext) -> {
              bannerMapper.disableExpiredBanners();
              return RepeatStatus.FINISHED;
            })
            .build();
  }
}

스케줄러 작성

src/main/java/kr/co/geniatutor/batch/SchedulerConfig.java 파일을 작성합니다.

package kr.co.geniatutor.batch;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

@Configuration
@EnableScheduling
public class SchedulerConfig {

    @Autowired
    private JobLauncher jobLauncher;

    @Autowired
    private Job bannerJob;

    @Scheduled(cron = "0 0 0 * * *")
    public void runBannerJob() throws Exception {
    	// 스프링 배치는 기본적으로 동시에 같은 Job이 중복 실행되지 않도록 방지하므로,
    	JobParameters jobParameters = new JobParametersBuilder()
    	        .addLong("time", System.currentTimeMillis()) // 매번 다른 파라미터 추가
    	        .toJobParameters();
    	
        jobLauncher.run(bannerJob, jobParameters);
    }
}

@EnableScheduling 어노테이션을 단 class는 SpringApplication이 실행될 때 빈으로 등록되고,
@Scheduled가 붙은 메서드를 찾아서 스케줄러에 등록합니다.

cron 스케줄을 0 0 0 * * * 로 설정하면 매일 자정 0시 Job이 실행됩니다.

스프링 배치 활성화

src/main/java/kr/co/geniatutor/GeniaBatchApplication.java 파일에 @EnableBatchProcessing를 추가합니다.

package kr.co.geniatutor.batch;

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableBatchProcessing
public class GeniaBatchApplication {

	public static void main(String[] args) {
		SpringApplication.run(GeniaBatchApplication.class, args);
	}

}

스프링 배치 미활성화 시 실행 에러메세지

***************************
APPLICATION FAILED TO START
***************************

Description:

Field jobLauncher in kr.co.geniatutor.batch.SchedulerConfig required a bean of type 'org.springframework.batch.core.launch.JobLauncher' that could not be found.

The injection point has the following annotations:
	- @org.springframework.beans.factory.annotation.Autowired(required=true)


Action:

Consider defining a bean of type 'org.springframework.batch.core.launch.JobLauncher' in your configuration.

Leave a comment