배치 실행 어플리케이션이란 개발된 배치 응용 프로그램을 스케줄링하여 실행이 가능하도록 해주는 어플리케이션으로 배치 실행 어플리케이션에서 각 개별 배치 응용 프로그램을 등록, 스케줄링하여 Process Fork 하는방식으로 실행하고 실행이력을 저장하는 기능을 수행하는 Stand Alone 어플리케이션이다.
이번 가이드를 통해서 배치 실행 어플리케이션을 설치할 수 있다. 설치 이후에, 파일 압축 Job을 수행하는 배치 워크플로우를 생성하고, 실행하는 과정을 통해서 간단하게 배치 실행 어플리케이션을 사용해 볼 수 있다.
배치 워크플로우란 하나의 배치업무처리를 여러 배치 프로그램들의 플로우로 구성한 집합을 의미한다.
사전 준비
다음은 케모마일 배포 파일(Chamomile.zip)이 C드라이브 Chamomile 폴더에 배포된 윈도우 컴퓨터에서 실행할 떄 를 설명한다.
MySQL(MariaDB)는 설치되어 있다고 가정한다.
Java 8 버전을 설치한다.
데이터베이스 계정 생성
데이터베이스를 생성한다.
데이터베이스 이름: chamomile
어플리케이션 설치 및 데이터베이스 설정
Chamomile.zip 파일을 C드라이브에 압축 해제한다.
압축 해제 후 생성된 Chamomile/chamomile-batch-2.3.0-RELEASE/conf 폴더에 위치한 application.properties
파일에서 datasource 설정 정보를 수정한다.
2번 항목의 2.3.0은 배치 실행 어플리케이션의 버전을 의미한다.
이번 가이드 문서에서는 2.3.0로 통일하도록 하며, 실제 설치 시에는 알맞은 버전인지 확인 후 설치하도록 한다.
# ...중략
################################################################################
Database Connection (MYSQL)
################################################################################
# Common connection pool
dataSource.common.driver=com.mysql.cj.jdbc.Driver
dataSource.common.log4jdbc.driver=net.sf.log4jdbc.DriverSpy
dataSource.common.url=jdbc:mysql://[데이터베이스서버]:3306/chamomile?serverTimezone=UTC
dataSource.common.log4jdbc.url=jdbc:log4jdbc:mysql://localhost:3306/chamomile?serverTimezone=UTC
dataSource.common.username=[사용자]
dataSource.common.password=[패스워드]
dataSource.common.initialSize=5
dataSource.common.maxActive=10
dataSource.common.validationQuery=select 1 from dual
dataSource.common.testWhileIdle=false
# ...
데이터베이스 테이블 생성
Chamomile/chamomile-batch-2.3.0-RELEASE/sql/ddl 폴더에 위치한 chamomile-batch.mysql.sql 파일을
참고해서 테이블을 생성한다.
이때, Chamomile online을 설치한 이력이 없어 DB에 CHMM_USER_INFO, CHMM_USER_ROLE_MAP
테이블이 존재하지 않는 경우, chamomile-batch.mysql.sql 파일을 참고해서,
해당 부분 주석을 해제하고 테이블을 생성한다.
기본 계정 정보를 테이블에 추가한다. (ID: admin, password: 1111)
아래 sql은 Chamomile/chamomile-batch-2.3.0-RELEASE/sql/data/chamomile-batch.data.mysql.sql
에서 확인할 수 있다.
INSERT INTO CHMM_USER_INFO (USER_ID,USER_PWD,USER_NAME,USE_YN) VALUES ('ADMIN','27d14effa31a772b2ee217b8c1b3a025fd9f888cb08fbf00dbc4970700b2dbe4ff501e29c7b853ab','관리자','1');
INSERT INTO CHMM_USER_ROLE_MAP (USER_ID,ROLE_ID,USE_YN) VALUES ('ADMIN','ROLE_BATCH_ADMIN','1');
Chamomile/chamomile-batch-2.3.0-RELEASE/sql/spring-batch 폴더에 위치한 spring-batch.mysql.sql 파일을
참고해서 테이블을 생성한다.
라이선스 적용
제공받은 라이선스 파일을 chamomile-batch-2.3.0/conf 폴더에 위치시킨다.
배치 실행 어플리케이션 실행
Chamomile/chamomile-batch-2.3.0-RELEASE 폴더에 위치한 application.bat 파일을 실행한다.
Chamomile/chamomile-batch-2.3.0-RELEASE/compress 폴더로 이동하여 outputFile.tar.gz 파일을 확인한다.
실행 이력 조회
배치워크플로우 상세 내역의 하단에서 워크플로우 실행 이력을 확인할 수 있다.
또한 실행이력 조회 메뉴에서 실행이력을 조회할 수 있다.
배치 프로젝트 생성 및 개발
DB에서 데이터를 읽어 파일로 생성하는 간단한 Job을 만들어 테스트를 진행한다.
프로젝트 생성
프로젝트 정보입력
먼저 File -> New -> Project를 선택한 후 Chamomile Framework -> Create Chamomile Project를 선택한 후 Next를 클릭한다.
아래와 같이 창이 생성되면 값을 입력해준다.
Project Name : edu-batch
Group ID : net.lotte.sample
Artifact ID : edu-batch
Version : 1.0.0
Base package :net.lotte.sample
생성할 프로젝트는 chamomile batch jobs 를 선택하고 Next버튼을 클릭한다.
Database정보 입력
생성될 프로젝트의 Database정보를 입력해야 한다. 미리 설정해 둔 Datasource의 목록이 표시되며 콤보박스에서 root를 선택하고 Connection test..를 클릭하여 정상적으로 연결이 되는지 확인한다. 이후 Finish버튼으로 프로젝트 생성을 완료한다.
![프로젝트 생성_DB설정](media/프로젝트 생성_DB설정.png)
발급받은 라이선스 파일을 conf 폴더로 복사한다.
코드 생성
생성된 프로젝트 -> 마우스 오른쪽 버튼 -> Chamomile -> generate Batch Job -> OK
Job 생성도구에서 다양한 배치 유형에 대해 코드를 생성할 수 있는 기능을 제공하고 있으며, 이번 예시에서는 DB to File을 선택하여 만들어 보도록 한다.
배치 유형 : DB to File
VO 생성 방식 : 테이블에서 직접 생성
Job name : exportBoard
Package name : net.lotte.sample.board
VO 정보 입력을 위해 스키마(chamomile)와 테이블(demo_board)을 선택한 후 Next
Job에서 동작하는 Reader는 Jdbc, Stored Procedure, MyBatis 를 이용하여 처리될 수 있으며, Writer 는 Delimited, Formatter, Json, XML 형식을 지원하며, 아래와 같이 입력 후 Finish를 선택한다.
Reader : JdbcCursor ItemReader
Reader Query : select * from demo_board
Writer : Json ItemWriter
테스트
소스코드 생성 시 배치 Job을 손쉽게 테스트 하기 위한 Junit 테스트 코드를 자동으로 생성한다.
src/test/java 폴더 내 net.lotte.sample.board.exportBoardTest를 선택하여 테스트를 수행한다.
테스트는 소스코드에서 우클릭 > Run As > JUnit Test를 선택하여 실행한다.
정상적으로 실행이 완료된 경우 demo_board 테이블의 정보가 명시 된 output 파라매터의 값에 따라 파일로 생성된 것을 확인할 수 있다.
프로젝트에서 우클릭 > Run As > 5 Maven Build...를 선택 하여 생성 된 위자드에서 Goals를 clean package를 입력한 후 실행한다.
아래와 같이 빌드 로그가 발생하고, 프로젝트의 target/edu-batch-1.0.0.jar 파일이 생성된 것을 확인한다.
...
[INFO] Reading assembly descriptor: assembly.xml
[INFO] Building zip: C:\Chamomile\workspace\edu-batch\target\edu-batch-1.0.0-dist.zip
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 24.483 s
[INFO] Finished at: 2022-04-05T17:22:58+09:00
[INFO] ------------------------------------------------------------------------
배치 실행 어플리케이션이 설치 된 디렉토리로 이동하여 deploy 폴더를 생성하고 jar 파일을 복사한다.
결과는 아래와 같다.
각각의 배치 잡들은 별도의 프로세스로 동작하기에 재시작은 필요 없다.
워크플로우 등록 및 실행
이제 배포 된 job을 이용하여 워크플로우를 만들어 실행해 보자.
+ 버튼을 클릭하여 새로운 배치 워크플로우를 생성한다.
# 표시된 항목은 필수 입력 값이므로 모두 입력한다.
아이디: EXPORT_BOARD (아이디의 경우 중복 검사를 해준다.)
이름: export board workflow
스케쥴 사용여부: 미사용
+버튼을 클릭하여 Job을 생성한다.
생성된 Job(회색 사각형)을 클릭한 이후에, 위와 같이 입력해준다.
아이디: export-board
이름: export-board job
유형: 유형 항목에서 SPRING(spring-batch)을 선택
명령어: /net/lotte/sample/board/exportBoard.xml$exportBoard
변수:
{
"output": "exportedBoard.json"
}
로그 레벨: INFO
입력이 끝나면, 상단에 저장버튼을 클릭한다.
SPRING(spring-batch)의 경우: spring batch 설정이 기술된 XML의 job의 경로를 기술한다.
명령어 설정 규칙은 {spring-batch XML 경로} + 구분자($) + {spring batch job아이디} 이다.
현재 만들어진 Job의 설정파일의 경로는 /net/lotte/sample/board/exportBoard.xml 이고 job의 아이디는 exportBoard 이기 때문에 /net/lotte/sample/board/exportBoard.xml$exportBoard 입력 된다.
/net/lotte/sample/board/exportBoard.xml 파일 내 job id 확인
<beans...
<job id="exportBoard" ...
해당 워크플로우를 실행하여 결과를 확인한다.
아래와 같이 exportedBoard.json 파일에 생성 된 데이터를 확인할 수 있다.
배치 개발 가이드
File DB 공통_개발가이드
배치 개발 절차
절차
배치 업무 개발은 Spring Batch를 기반으로 한다.
![image](media/배치 개발 절차.png)
배치 개발 Flow
기능 정의 (전문 정의)
배치설계서를 통해 필요한 기능을 도출한다.
배치 유형을 정의한다. (Tasklet, Job-Step)
Input, Output을 정의한다. (전문정의)
선후행 작업을 정의한다.
XML 작성
Job과 Step을 정의하고 관계를 작성한다.
기능정의를 통해 설계한 내용을 바탕으로 XML을 작성한다.
Java Class 작성
Job을 구성하는 Step의 Class를 작성한다.
JUnit 단위 테스트
Eclipse Plug-In에서 배치작업에 대한 단위테스트 코드를 생성하고 테스트를 진행한다.
Junit Test Class 생성 및 단위 테스트를 진행한다.
배치 개발 가이드
배치 코딩 가이드> FILE, DB공통
배치 JAVA 기본 구조
아래와 같이 기본 구조를 생성한다.
(File과 DB 모두 Java의 형태는 itemReader, itemProcessor, itemWriter로 구성된다.)
VO는 같은 Java파일에 구성해도 되고, VO만을 담은 별도의 Java파일로 분리해도 된다.
package net.lotte.chamomile.batch.template.example.spring.filetofile;
import javax.sql.DataSource;
..
@Configuration
public class FileToFile {
//VO 정의
public class Person {..}
//itemReader() 메소드이다.
FlatFileItemReader<Person> itemReader(@Value("#{jobParameters['in']}")String in) {..}
//itemProcessor() 메소드이다.
ItemProcessor<? super Person, ? extends Person> itemProcessor() {..}
//itemWriter() 메소드이다.
ItemStreamWriter<Person>
itemWriter(@Value("#{jobParameters['out']}")String out) {..}
}
배치 XML 기본 구조
Job과 Step을 정의한 XML을 작성한다.
(아래의 소스는 FILE에 대한 소스이며, DB 또한, FILE과 형태는 동일하다.)
public class ItemRecord {
private String id;
private String name;
private String email;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
배치 FILE 처리 유형
ItemReader
Delimited
FixedLength
Json
XML
ItemProcessor
ItemProcessor
xmlItemProcessor
ItemWriter
Delimited
FixedLength
Json
XML
**배치 코딩 가이드 > FILE **
ItemReader
1) Delimited File Reader
구분자로 컬럼이 분리된 파일 형식을 읽을 때 사용한다.
@Bean
@Scope("step")
@Value("#{jobParameters['input']}")
FlatFileItemReader<ItemRecord> delimitedFlatFileItemReader(String input) {
//파일 내 컴럼 구분자이다.
final String delimiter = ",";
//도메인에 정의된 멤버변수 이다.
final String names[] = {"id", "name", "email"};
logger.debug("input:" + input);
//Input 파일 설정한다.
Resource resource = new FileSystemResource(input);
//사전에 정의된 도메인 형식으로 초기화 한다.
FlatFileItemReader<ItemRecord> itemReader = new FlatFileItemReader<>();
itemReader.setResource(resource);
DefaultLineMapper<ItemRecord> lineMapper = new DefaultLineMapper<>();
//정의된 구분자로 초기화 한다.
DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer(delimiter);
lineTokenizer.setNames(names);
lineMapper.setLineTokenizer(lineTokenizer);
BeanWrapperFieldSetMapper<ItemRecord> fieldMapper = new BeanWrapperFieldSetMapper<>();
fieldMapper.setTargetType(ItemRecord.class);
lineMapper.setFieldSetMapper(fieldMapper);
itemReader.setLineMapper(lineMapper);
return itemReader;
}
2) FixedLength File Reader
고정 컬럼 길이 파일 형식을 읽을 때 사용한다.
@Bean
@Scope("step")
@Value("#{jobParameters['input']}")
FlatFileItemReader<ItemRecord> fixedLengthFlatFileItemReader(String input) {
//도메인에 정의된 멤버변수 이다.
final String names[] = {"id", "name", "email"};
//고정 컬럼 길이를 정의한다.
final Range[] ranges = {new Range(1, 4), new Range(5, 10), new Range(11, 26)};
logger.debug("input:" + input);
//Input 파일 설정한다.
Resource resource = new FileSystemResource(input);
//사전에 정의된 도메인 형식으로 초기화 한다.
FlatFileItemReader<ItemRecord> itemReader = new FlatFileItemReader<>();
itemReader.setResource(resource);
DefaultLineMapper<ItemRecord> lineMapper = new DefaultLineMapper<>();
//정의한 구분자 및 고정 컬럼 길이로 초기화 한다.
FixedLengthTokenizer lineTokenizer = new FixedLengthTokenizer();
lineTokenizer.setNames(names);
lineTokenizer.setColumns(ranges);
lineMapper.setLineTokenizer(lineTokenizer);
BeanWrapperFieldSetMapper<ItemRecord> fieldMapper = new BeanWrapperFieldSetMapper<>();
fieldMapper.setTargetType(ItemRecord.class);
lineMapper.setFieldSetMapper(fieldMapper);
itemReader.setLineMapper(lineMapper);
return itemReader;
}
3) Json File Reader
JSON 파일 형식을 읽을 때 사용한다.
각 라인에 JSON 객체가 들어간다.
(JSON ARRAY 일 지라도 한 라인에 들어가 있으면 하나의 레코드로 인식한다.)
@Bean
@Scope("step")
FlatFileItemReader<ItemRecord> itemReader(@Value("#{jobParameters['input']}")String input) throws Exception {
FlatFileItemReader<ItemRecord> itemReader = new FlatFileItemReader<>();
Resource resource = new FileSystemResource(input);
itemReader.setResource(resource);
LineMapper<Person> lineMapper = new LineMapper<Person>() {
//JsonConverter 를 이용하여 Json 파일을 파싱한다.
@Override
public Person mapLine(String line, int lineNumber) throws Exception {
Person record = JsonConverter.convertJsonToObject(line, ItemRecord.class);
return record;
}
};
itemReader.setLineMapper(lineMapper);
return itemReader;
}
4) XML File Reader
XML 파일 형식을 읽을 때 사용한다.
@Bean
@Scope("step")
@Value("#{jobParameters['input']}")
StaxEventItemReader<XmlRecord> xmlStaxEventItemReader(String input) {
logger.debug("input:" + input);
Resource resource = new FileSystemResource(input);
StaxEventItemReader<XmlRecord> itemReader = new StaxEventItemReader<>();
itemReader.setResource(resource);
//XML 파일에 레코드 단위 Root Element를 정의한다.
itemReader.setFragmentRootElementNames(new String[] {"item"});
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(XmlRecord.class);
itemReader.setUnmarshaller(marshaller);
return itemReader;
}
ItemProcessor
1) ItemProcessor
Reader에서 가져온 레코드를 처리한다.
@Bean
ItemProcessor<? super ItemRecord, ? extends ItemRecord> itemProcessor() {
ItemProcessor<ItemRecord, ItemRecord> itemProcessor = new ItemProcessor<ItemRecord, ItemRecord>(){
//레코드에 대한 비즈니스 로직 처리이다.
@Override
public ItemRecord process(ItemRecord record) {
logger.debug(TextTableBuilder.build(record));
return record;
}
};
return itemProcessor;
}
2) ItemProcessor (XML 형식)
itemReader에서 추출한 레코드를 처리한다. (XML 파일 형식에 대한 itemReader)
@Bean
ItemProcessor<? super XmlRecord, ? extends XmlRecord> xmlItemProcessor() {
ItemProcessor<XmlRecord, XmlRecord> itemProcessor = new ItemProcessor<XmlRecord, XmlRecord>(){
//레코드에 대한 비즈니스 로직 처리 이다.
@Override
public XmlRecord process(XmlRecord record) {
logger.debug(TextTableBuilder.build(record));
return record;
}
};
return itemProcessor;
}
ItemWriter1) Delimited File Writer
구분자로 분리된 형식의 파일을 출력한다.
@Bean
@Scope("step")
ItemStreamWriter<ItemRecord> delimitedFlatFileItemWriter(@Value("#{jobParameters['output']}")String output) {
//도메인에 정의된 멤버변수이다.
final String delimiter = ",";
//출력할 파일 내의 컬럼 구분자이다.
final String names[] = {"id", "name", "email"};
logger.debug("output:" + output);
FlatFileItemWriter<ItemRecord> itemWriter = new FlatFileItemWriter<>();
FileSystemResource resource = new FileSystemResource(output);
itemWriter.setResource(resource);
itemWriter.setShouldDeleteIfExists(true);
DelimitedLineAggregator<ItemRecord> lineAggregator = new DelimitedLineAggregator<>();
lineAggregator.setDelimiter(delimiter);
BeanWrapperFieldExtractor<ItemRecord> fieldExtractor = new BeanWrapperFieldExtractor<>();
fieldExtractor.setNames(names);
lineAggregator.setFieldExtractor(fieldExtractor);
itemWriter.setLineAggregator(lineAggregator);
return itemWriter;
}
2) Formatter File Writer
정의된 포맷으로 파일을 출력한다.
@Bean
@Scope("step")
ItemStreamWriter<ItemRecord> formatterFlatFileItemWriter(@Value("#{jobParameters['output']}")String output) {
//출력포맷을 정의한다.
final String names[] = {"id", "name", "email"};
//도메인에 정의된 멤버변수이다.
final String format = "%-5s%-10s%20s";
logger.debug("output:" + output);
FlatFileItemWriter<ItemRecord> itemWriter = new FlatFileItemWriter<>();
FileSystemResource resource = new FileSystemResource(output);
itemWriter.setResource(resource);
itemWriter.setShouldDeleteIfExists(true);
FormatterLineAggregator<ItemRecord> lineAggregator = new FormatterLineAggregator<>();
lineAggregator.setFormat(format);
BeanWrapperFieldExtractor<ItemRecord> fieldExtractor = new BeanWrapperFieldExtractor<>();
fieldExtractor.setNames(names);
lineAggregator.setFieldExtractor(fieldExtractor);
itemWriter.setLineAggregator(lineAggregator);
return itemWriter;
}
3) Json File Writer
JSON 형식으로 파일을 출력한다.
@Bean
@Scope("step")
ItemStreamWriter<ItemRecord> jsonFlatFileItemWriter(@Value("#{jobParameters['output']}")String output) {
logger.debug("output:" + output);
FlatFileItemWriter<ItemRecord> itemWriter = new FlatFileItemWriter<>();
FileSystemResource resource = new FileSystemResource(output);
itemWriter.setResource(resource);
itemWriter.setShouldDeleteIfExists(true);
LineAggregator<ItemRecord> lineAggregator = new LineAggregator<ItemRecord>(){
@Override
public String aggregate(ItemRecord item) {
String json = null;
try {
//Json 형태로 Converter 한다.
json = JsonConverter.convertObjectToJsonLine(item);
} catch (JsonProcessingException e) {
logger.error("JsonProcessingException:" + e.toString());
throw new RuntimeException(e);
}
return json;
}
};
itemWriter.setLineAggregator(lineAggregator);
return itemWriter;
}
4) XML File Writer
XML 형식으로 파일을 출력한다.
@Bean(destroyMethod="")
@Scope("step")
StaxEventItemWriter<ItemRecord> xmlStaxEventItemWriter(@Value("#{jobParameters['output']}")String output) {
//root 엘리먼트를 정의한다.
final String rootTagName = "root";
logger.debug("output:" + output);
StaxEventItemWriter<ItemRecord> itemWriter = new StaxEventItemWriter<>();
FileSystemResource resource = new FileSystemResource(output);
itemWriter.setResource(resource);
itemWriter.setRootTagName(rootTagName);
itemWriter.setOverwriteOutput(true);
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(ItemRecord.class);
itemWriter.setMarshaller(marshaller);
return itemWriter;
}
5) XML File Writer (입력이 XML 형식의 Record인 경우 사용)
XML 형식으로 파일을 출력한다.
@Bean(destroyMethod="")
@Scope("step")
StaxEventItemWriter<XmlRecord> xmlStaxEventXMLItemWriter(@Value("#{jobParameters['output']}")String output) {
//root 엘리먼트를 정의한다.
final String rootTagName = "root";
logger.debug("output:" + output);
StaxEventItemWriter<XmlRecord> itemWriter = new StaxEventItemWriter<>();
FileSystemResource resource = new FileSystemResource(output);
itemWriter.setResource(resource);
itemWriter.setRootTagName(rootTagName);
itemWriter.setOverwriteOutput(true);
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(XmlRecord.class);
itemWriter.setMarshaller(marshaller);
return itemWriter;
}
배치 DB 처리 유형
ItemReader
JdbcCursorItemReader
StoredProcedureItemReader
JdbcPagingItemReader
MybatisCursorItemReader
MybatisPagingItemReader
ItemProcessor
ItemProcessor
ItemWriter
JdbcBatchItemWriter
JdbcXmlItemWriter
MybatisBatchItemWriter
배치 코딩 가이드 > DB
ItemReader
1) JdbcCursorItemReader
Jdbc Cursor로 Query를 실행하여 Record를 가져온다.
@Bean
ItemReader<ItemRecord> jdbcCursorItemReader() {
//JdbcCursorItemReader 선언한다.
JdbcCursorItemReader<ItemRecord> itemReader = new JdbcCursorItemReader<>();
itemReader.setDataSource(dataSource);
itemReader.setSql(
"select id, name, email "
+ "from chmm_bat_exam_filetodb "
+ "where rownum <= 10"
); /* SQL문을 설정한다. */
logger.debug("sql:" +itemReader.getSql());
/* 필요한 곳에 logger를 사용한다. */
BeanPropertyRowMapper<ItemRecord> rowMapper = new BeanPropertyRowMapper<>();
rowMapper.setMappedClass(ItemRecord.class);
//Row Mapping 및 Return 한다.
itemReader.setRowMapper(rowMapper);
return itemReader;
}
2) StoredProcedureItemReader
Stored Procedure를 실행하여 Record를 가져온다.
@Bean
ItemReader<ItemRecord> storedProcedureItemReader() {
final String sprocedureName = "SELECT_CHMM_BAT_EXAM_FILETODB";
/* Stored Procedure 명을 정의한다. */
StoredProcedureItemReader<ItemRecord> itemReader = new StoredProcedureItemReader<>();
itemReader.setDataSource(dataSource);
itemReader.setProcedureName(sprocedureName);
/*
* 각 DB에 맞는 Return Cursor를 정의해야 한다.
* 1.ResultSet (SQL Server, Sybase, DB2, Derby and MySQL)
* 2.ref-cursor (Oracle, PostgreSQL)
* 3.함수일 경우 Return Value
*/
SqlParameter[] parameters = {new SqlParameter("cur", oracle.jdbc.OracleTypes.CURSOR)}; /* for ORACLE */
itemReader.setParameters(parameters);
itemReader.setRefCursorPosition(1); /* for ORACLE */
logger.debug("sql:" + itemReader.getSql());
/* 필요한 곳에 logger를 사용한다. */
BeanPropertyRowMapper<ItemRecord> rowMapper = new BeanPropertyRowMapper<>();
rowMapper.setMappedClass(ItemRecord.class);
itemReader.setRowMapper(rowMapper);
return itemReader;
}
3) JdbcPagingItemReader
- Jdbc Paging Query를 실행하여 Record를 가져온다.
@Bean
ItemReader<ItemRecord> jdbcPagingItemReader() throws Exception {
final int pageSize = 10;
/* 한 Page에 가져올 Row 수를 정의한다. */
JdbcPagingItemReader<ItemRecord> itemReader = new JdbcPagingItemReader<>();
itemReader.setDataSource(dataSource);
SqlPagingQueryProviderFactoryBean factory = new SqlPagingQueryProviderFactoryBean();
factory.setDataSource(dataSource);
factory.setDatabaseType("ORACLE");
factory.setSelectClause("select ID, NAME, EMAIL");
factory.setFromClause("from CHMM_BAT_EXAM_FILETODB");
factory.setWhereClause("where 1=1");
factory.setSortKey("ID");
PagingQueryProvider queryProvider = (PagingQueryProvider) factory.getObject();
itemReader.setQueryProvider(queryProvider);
itemReader.setPageSize(pageSize);
BeanPropertyRowMapper<ItemRecord> rowMapper = new BeanPropertyRowMapper<>();
rowMapper.setMappedClass(ItemRecord.class);
itemReader.setRowMapper(rowMapper);
return itemReader;
}
@Bean
public ItemWriter<MybatisVO> myBatisBatchItemWriter() {
MyBatisBatchItemWriter<MybatisVO> itemWriter = new MyBatisBatchItemWriter<MybatisVO>();
itemWriter.setSqlSessionFactory(oracleSqlSessionFactory);
itemWriter.setStatementId("insertDBTODB");
/* Query ID를 설정한다. */
return itemWriter;
}
Junit 테스트 가이드
배치업무는 단위 테스트를 위해 Junit 테스트 방법을 제공한다.
이는 개발된 업무 배치 만을 단위 테스트 한다.
Junit Test Class 생성
<모듈명>.java 생성 : 테스트에서 사용할 Class를${BATCH_HOME}/src/test//<모듈명>에 생성한다.
Test Class를 아래와 같이 작성한다.
package net.lotte.chamomile.batch.example.spring.dbtodb;
import org.junit.Test;
import net.lotte.chamomile.batch.launcher.spring.SpringJobLauncher;
public class DbToDbTest {
@Test
public void test() throws Exception {
try {
//Job 및 Step이 정의되어 있는 XML 이다.
//$personJobs는테스트 Job ID 이다.
String command = "/net/lotte/chamomile/batch/example/spring/dbtodb/DbToDb.xml$personJob";
SpringJobLauncher.test(command,null);
assert(true);
}catch(Exception e) {
e.printStackTrace(System.err);
assert(false);
}
}
}
JUnit Test 실행 (Run)
앞의 과정이 완료되면 각 업무배치 별 JUnit Test가 가능하다.
Test를 위한 방법은 다음과 같다.
STEP 1.
Navigation : 업무 배치 Class -> 마우스 오른쪽 클릭 -> Run As -> JUnit Test 선택
STEP 2.
해당 JUnit Adapter Class가 최초 실행됬을 경우 이클립스 설정에 따라 Launcher 선택화면이 나타날 수 있다. Launcher 화면에서 “Eclipse JUnit Launcher” 를 선택하고 “OK”
JUnit Test 로그 확인
JUnit Test가 실행되면 Eclipse의 Console창에 로그가 출력된다.
로그의 형태는 로그 세팅과 Adpater 로그에 따라 다소 차이가 있을 수 있다.
Mybatis_개발가이드
구성도
배치 코딩 가이드 > Mybatis
VO 작성
DB 컬럼에 대한 VO(Value Object)를 작성한다.
DAO에서 작성된 property명과 일치해야 한다.
변수 정의와 함께 Getter와 Setter 매소드를 정의해야 한다.
package net.lotte.chamomile.batch.example.spring.mybatis;
public class MybatisVO {
private String id;
private String name;
private String email;
private String inout;
private String create_dt;
public String getUserNm() {
return userNm;
}
public void setUserNm(String userNm) {
this.userNm = userNm;
}
//중략..
}
DAO 작성 (SQL Query.. .xml)
SQL 쿼리문을 통해 데이터 Fetch, Insert가 가능한 부분이다.
경로를 적는 부분(namespace, type 등)에 올바른 경로가 들어갈 수 있도록 한다.
각 쿼리(select, insert)별 ID는 Reader, Processor, Writer정의된 java에서 매핑하여 사용하는 부분이므로 타 사용자가 보기에도 명확하게 이해할 수 있도록 ID를 정한다.
각각의 DB별로 쿼리문이 상이하오니 쿼리문 작성에 유의하여야 한다.
VO를 통해 특정 값을 삽입할 경우, “#{}”를 이용해 값을 넣을 수 있다.
<?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="net.lotte.chamomile.batch.example.spring.mybatis.MybatisDao">
<resultMap id="resultUsers" type="net.lotte.chamomile.batch.example.spring. mybatis.MybatisVO">
<result column="USER_NM" property="userNm"/>
<result column="USER_PW" property="userPw"/>
<result column="ENABLED" property="enabled"/>
</resultMap>
<select id="selectUsers" resultMap="resultUsers">
SELECT USER_NM AS USER_NM
,USER_PW AS USER_PW
,ENABLED AS ENABLED
FROM TEST.DBO.USERS
</select>
<insert id="insertUsers">
INSERT
INTO TEST.DBO.USERS
VALUES (#{userNm}, #{userPw}, #{enabled})
</insert>
</mapper>
JOB, STEP 정의 XML 작성
JOB, STEP을 정의하고, Reader, Processor, Writer를 정의한다.
(JOB과 STEP의 순서 정의와 배치 트랜잭션에 대한 설정을 할 수 있다.)
Spring Batch의 기본 기능인 트랜잭션 설정이 필요한 경우, 추가로 설정한다.
(commit-interval, next 등)
Datasource의 value값을 Mapping시키기 위해 별도의 DB Properties 파일로 분리하였다.
기본적인 DB Properties 파일들은 다음과 같다.
(특정 DB에 대한 Url, User, Password등 정보는 각 프로젝트의 상황에 맞춰 작성하면 된다.)
# Oracle DB Properties (conf/batch-multidb/batch-oracle.properties)
oracle.jdbc.driver=oracle.jdbc.OracleDriver
oracle.jdbc.url=jdbc:oracle:thin:@접속IP:접속Port:오라클버전
oracle.jdbc.user=User ID
oracle.jdbc.password=Password
# MySQL DB Properties (conf/batch-multidb/batch-mysql.properties)
mysql.jdbc.driver=com.mysql.jdbc.Driver
mysql.jdbc.url=jdbc:mysql://접속IP:접속Port/SID명
mysql.jdbc.user=User ID
mysql.jdbc.password=Password
# Maria DB Properties (conf/batch-multidb/batch-maria.properties)
maria.jdbc.driver=org.mariadb.jdbc.Driver
maria.jdbc.url=jdbc:mariadb://접속IP:접속Port/SID명
maria.jdbc.user=User ID
maria.jdbc.password=Password
# Tibero DB Properties (conf/batch-multidb/batch-tibero.properties)
tibero.jdbc.driver=com.tmax.tibero.jdbc.TbDriver
tibero.jdbc.url=jdbc:tibero:thin:@접속IP:접속Port:SID명
tibero.jdbc.user=User ID
tibero.jdbc.password=Password
# MS-SQL DB Properties (conf/batch-multidb/batch-mssql.properties)
mssql.jdbc.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
mssql.jdbc.url=jdbc:sqlserver://접속IP:1433;DatabaseName=SID명;
mssql.jdbc.user=User ID
mssql.jdbc.password=Password
DB Connection 설정
DB Connection 시 사용하게 될 Connection 정보를 작성
각 DB별 Connection 설정은 ${mssql.} , ${oracle.}과 같이 사용하면, 각 DB별로 설정된다.
ref는 각 DB별로 mssqlDatasource, mysqlDatasource 등으로 정의
Read Listener가 2개, 혹은 3개로 달라지는 것을 볼 수 있다. (commit-interval의 이해를 돕기 위한 부분으로 Read부분만 나타낸다.)
Configuring Step Restart
Configuring Step Restart에 대한 XML 작성
“allow-start-if-complete (true / false)” 옵션을 사용하여 적용한다. (Default : False)
(True : Step이 성공(complete)적으로 끝난 경우에도 해당 Step에 대해 재실행이 필요한 경우이다. False : 실패한 Step에 대해서만 재실행한다. (성공한 Step은 Skip))
- Write 시 Write를 할 수 없다는 에러를 발생시켜야 하나
“no-rollback-exception-classes” 사용을 통해 에러가 발생되지 않는 것을 볼 수 있다.
아래 결과 화면은 DB의 내용에 대한 Read만 된 후 배치가 종료된 것을 확인할 수 있다.)
존재하지 않는테이블명기재로 인한Exception발생
위 오류 발생 시 Exception이 발생되지 않게 “no-rollback-exception-classes” 설정 후 실행하면, 위 Exception에러가 발생되지 않는다.
Configuring Listener (ItemReadListener)
ItemReadListener 생성
각 Item별 Read와 관련하여 호출되며, Commit-Interval 기준으로 Listener가 호출된다.
//ItemReaderListener.java
package net.lotte.chamomile.batch.example.spring.dbtodb.Listener;
import org.springframework.batch.core.ItemReadListener;
public class ItemReaderListener<Domain> implements ItemReadListener<Domain> {
@Override
//item read 전 호출
public void beforeRead() {
System.out.println("ItemReadListener - beforeRead");
}
@Override
//item read 후 호출
public void afterRead(Domain item) {
System.out.println("ItemReadListener - afterRead");
}
@Override
//item read error 시 호출
public void onReadError(Exception ex) {
System.out.println("ItemReadListener - onReadError");
}
}
Configuring Listener (StepListener)
StepListener 생성
Step과 관련하여 호출되며, Step 기준으로 Listener가 호출된다.
//StepListener.java
package net.lotte.chamomile.batch.example.spring.dbtodb.Listener;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepExecutionListener;
public class StepListener implements StepExecutionListener {
@Override
//step 시작전 호출
public void beforeStep(StepExecution stepExecution) {
// TODO Auto-generated method stub
System.out.println("@@@ Before Listener - Before Step");
}
@Override
//step 종료 후 호출
public ExitStatus afterStep(StepExecution stepExecution) {
// TODO Auto-generated method stub
System.out.println("@@@ After Listener - After Step");
return null;
}
}
Configuring Listener (ItemWriterListener)
ItemWriterListener 생성
각 Item별 Write와 관련하여 호출되며, Commit-Interval 기준으로 Listener가 호출된다.
//ItemWriterListener.java
package net.lotte.chamomile.batch.example.spring.dbtodb.Listener;
import java.util.List;
import org.springframework.batch.core.ItemWriteListener;
public class ItemWriterListener<Domain> implements ItemWriteListener<Domain> {
@Override
//item write 전 호출
public void beforeWrite(List<? extends Domain> items) {
System.out.println("ItemWriteListener - beforeWrite");
}
@Override
//item write 후 호출
public void afterWrite(List<? extends Domain> items) {
System.out.println("ItemWriteListener - afterWrite");
}
@Override
//item write error 시 호출
public void onWriteError(Exception exception, List<? extends Domain> items) {
System.out.println("ItemWriteListener - onWriteError");
}
}
Listener 테스트를 위한 XML 작성
Read, Write, Step 관련 Listener를 테스트 하기 위하여 Listener.xml을 작성한다.
package net.lotte.chamomile.batch.template.spring.filetodb;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FileToDb {
/** Logger */
protected final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
DataSource dataSource;
}
상세 구조 생성
배치 유형에 따라 상세 구조를 생성한다. (3-2 배치 기능별 가이드 참고)
Restartablility 설정
적용 시나리오
Job의 재기동 여부를 제한하고자 할 때 사용한다. 기본적으로 Spring Batch의 Job은 Job Parameter 별로 Job Instance가 생성되고. 실행 단위가 된다. 정상종료(COMPLETED)로실행된 Job Instance는 재실행 되지 않으나(Restartablility가 설정되어 있다 하더라도), 오류(FAILED)나 중지(STOP) 처리된 Job Instance는 재실행이 가능하다.
이러한 재실행을 방지하고자 할 때 restartable을 false로 설정한다. (기본은 true)
적용 시나리오
Step의 실패(FAILED) 이후 재시작 가능한 횟수를 명시하고자 할 때 start-limit를 설정할 수 있다.
Limit 횟수를 초과하면org.springframework.batch.core.StartLimitExceededException이 발생한다. 기본값은 Integer.MAX_VALUE 이다.
적용 시나리오
plit Flow를 병렬로 처리 할 수 있다. 하나의 Job에 여러 Flow로 정의된 스텝이 있고, 상호 배타적인 관계라면, 이를 병렬로 동시에 처리하여 속도를 향상시킬 수 있다.
실제 작업 수행은 Multithreaded Step과 같이 task-executor가 담당한다.
Partitioning Step (single process or multiprocess: File to File)
적용 시나리오
다중 파일 입력이나, Partitioning 되어 있는 DB에서 Data를 가져와 사용할 때 Partitioning Step을 사용하면 속도를 향상시킬 수 있다.
Multithreaded Step의 throttle-limit와 유사하게 grid-size로 병렬 처리 크기를 제어할 수 있다.
동작 방식은 Partition Step(master step)이 grid-size 만큼의 slave Step을 생성하여 병렬처리한다.
Partition Step은 slave step이 종료될 때 까지 대기하고, 각 결과를 취합하여 처리한다.
ConcurrentTaskExecutor
이 구현체는 Java 5 java.util.concurrent.Executor의 래퍼다.
또 다른 대안은 빈 프로퍼티로 Executor 설정 파라미터를 노출하는 ThreadPoolTaskExecutor다. ConcurrentTaskExecutor를 사용해야 하는 경우는 드물지만 ThreadPoolTaskExecutor가 원하는 만큼 안정적이지 않다면 ConcurrentTaskExecutor를 대신 사용할 수 있다.
SimpleAsyncTaskExecutor
이 구현에는 어떤 스레드도 재사용하지 않고 호출마다 새로운 스레드를 시작한다. 하지만 이 구현체는 동시접속 제한(concurrency limit)을 지원해서 제한 수가 넘어서면 빈 공간이 생길 때까지 모든 요청을 막을 것이다. 실제 풀링(pooling)을 원한다면 뒷부분을더 봐야 한다.
SimpleThreadPoolTaskExecutor
이 구현체는 실제로 Quartz SimpleThreadPool의 하위클래스로 스프링의 생명주기 콜백을 받는다
이는 Quartz와 Quartz가 아닌 컴포넌트 간에 공유해야 하는 스레드 풀이 있는 경우에 보통 사용한다.
SyncTaskExecutor
이 구현체는 호출을 비동기적으로 실행하지 않고 대신, 각 호출이 호출 스레드에 추가된다.
간단한 테스트 케이스처럼 멀티스레드가 필요하지 않은 상황에서 주로 사용한다.
ThreadPoolTaskExecutor
이 구현체는 java.util.concurrent.ThreadPoolExecutor를 구성하는 빈 프로퍼티를 노출하고 이를 TaskExecutor로 감싼다. ScheduledThreadPoolExecutor같은 고급 기능이 필요하다면 ConcurrentTaskExecutor를 대신 사용하기를 권장한다.
Junit테스트 가이드
배치업무는 단위 테스트를 위해 Junit 테스트 방법을 제공한다. 이는 개발된 업무 배치 만을 단위 테스트 한다.
Junit Test Class 생성
<모듈명>.java 생성 : 테스트에서 사용할 Class를 ${BATCH_HOME}/src/test//<모듈명>에 생성한다.
Test Class를 아래와 같이 작성한다.
package net.lotte.chamomile.batch.template.spring.simultaneous;
import java.util.HashMap;
import java.util.Map;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.JobParameters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import net.lotte.chamomile.batch.test.JobLauncherTestUtils;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/simple-job-launcher-context.xml", "/jobs/SimultaneousMultithreadedStep.xml", "/job-runner-context.xml" })
public class SimultaneousMultithreadedStepTest {
private static final long JOB_PARAMETER_MAXIMUM = 1000000;
@Autowired
private JobLauncherTestUtils jobLauncherTestUtils;
@Test
public void testJob() throws Exception {
Assert.assertEquals(BatchStatus.COMPLETED, jobLauncherTestUtils.launchJob(getJobParameters()).getStatus());
}
public JobParameters getJobParameters() {
Map<String, JobParameter> parameters = new HashMap<String, JobParameter>();
parameters.put("input", new JobParameter("src/test/resources/person.txt"));
parameters.put("random", new JobParameter((long) (Math.random() * JOB_PARAMETER_MAXIMUM)));
return new JobParameters(parameters);
}
}
JUnit Test 실행 (Run)
앞의 과정이 완료되면 각 업무배치 별 JUnit Test가 가능하다.
Test를 위한 방법은 다음과 같다.
STEP 1.
Navigation : 업무 배치 Class -> 마우스 오른쪽 클릭 -> Run As -> JUnit Test 선택
STEP 2.
해당 JUnit Adapter Class가 최초 실행됬을 경우 이클립스 설정에 따라 Launcher 선택화면이 나타날 수 있다. Launcher 화면에서 “Eclipse JUnit Launcher” 를 선택하고 “OK”