이클립스 스프링 배치 프로젝트 생성 및 스케줄링 실행 방법
이클립스 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