💬
목차
< 뒤로가기
인쇄

배치 개발 가이드

개발가이드 version 2.3.0


배치 Getting Started

배치 실행 어플리케이션 설치

개요

배치 실행 어플리케이션이란 개발된 배치 응용 프로그램을 스케줄링하여 실행이 가능하도록 해주는 어플리케이션으로 배치 실행 어플리케이션에서 각 개별 배치 응용 프로그램을 등록, 스케줄링하여 Process Fork 하는방식으로 실행하고 실행이력을 저장하는 기능을 수행하는 Stand Alone 어플리케이션이다.

image-20200915154621951

이번 가이드를 통해서 배치 실행 어플리케이션을 설치할 수 있다. 설치 이후에, 파일 압축 Job을 수행하는 배치 워크플로우를 생성하고, 실행하는 과정을 통해서 간단하게 배치 실행 어플리케이션을 사용해 볼 수 있다.

배치 워크플로우란 하나의 배치업무처리를 여러 배치 프로그램들의 플로우로 구성한 집합을 의미한다.

사전 준비

다음은 케모마일 배포 파일(Chamomile.zip)이 C드라이브 Chamomile 폴더에 배포된 윈도우 컴퓨터에서 실행할 떄 를 설명한다.

MySQL(MariaDB)는 설치되어 있다고 가정한다.

Java 8 버전을 설치한다.

데이터베이스 계정 생성

데이터베이스를 생성한다.

  • 데이터베이스 이름: chamomile

어플리케이션 설치 및 데이터베이스 설정

  1. Chamomile.zip 파일을 C드라이브에 압축 해제한다.

  2. 압축 해제 후 생성된 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
# ...

데이터베이스 테이블 생성

  1. 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 파일을 참고해서,

    해당 부분 주석을 해제하고 테이블을 생성한다.

  1. 기본 계정 정보를 테이블에 추가한다. (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');
  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 파일을 실행한다.

[https://[설치서버호스트]:설정포트(https://설치서버호스트:설정포트)

application.xml에서 포트정보를 변경하지 않았다면 https://localhost:6100으로 웹콘솔에 접속한다.

로그인화면

로그인 화면이 정상적으로 열린다면 로그인한다.

Default 사용자/패스워드는 ADMIN / 1111 이다.

배치 워크플로우 관리

좌측 메뉴에서, 배치워크플로우 관리를 선택하여 메뉴에 진입할 수 있다.

항목별 자세한 설명은 배치_운영가이드를 참고한다.

배치 워크플로우 등록

배치워크플로우 관리 화면

+ 버튼을 클릭하여 새로운 배치 워크플로우를 생성한다.

배치워프클로우 상세내역 화면

# 표시된 항목은 필수 입력 값이므로 모두 입력한다.

아이디: COMPRESSION0001 (아이디의 경우 중복 검사를 해준다.)
이름: compression workflow
스케쥴 사용여부: 미사용

Job 생성

하나의 배치 워크플로우는 여러 개의 Job으로 구성될 수 있다. 이번 예시에서는 파일을 압축하는 Job을 생성한다.

Job 생성 화면

+버튼을 클릭하여 Job을 생성한다.

다음으로 압축할 파일을 준비한다.

Chamomile/chamomile-batch-2.3.0-RELEASE 폴더에 compress라는 이름으로 폴더를 생성한다.

compress 폴더 내에 inputFile.txt 파일을 생성한다.

inputFile.txt 파일의 경로는 Chamomile/chamomile-batch-2.3.0-RELEASE/compress/inputFile.txt 이다.

Job 상세 내역 화면

생성된 Job(회색 사각형)을 클릭한 이후에, 위와 같이 입력해준다.

아이디: COMPRESSION_JOB_0001
이름: compression job
유형: 유형 항목에서 Built-in-job을 선택하고 Compression을 선택한다.
명령어: /net/lotte/chamomile/batch/builtin/compression/Compression.xml$compressionJob
변수: 
{
"processType": "compress",
"sourceInput": "compress/inputFile.txt",
"sourceOutput": "compress/outputFile.tar.gz"
}
로그 레벨: INFO

입력이 끝나면, 상단에 저장버튼을 클릭한다.

배치 워크플로우 실행

배치 워크플로우 관리 메뉴로 이동하여 위에서 생성한 워크플로우를 조회한다.

그리고 실행 버튼을 클릭하여 배치워크플로우를 실행한다.

압축된 결과 파일을 확인한다.

Chamomile/chamomile-batch-2.3.0-RELEASE/compress 폴더로 이동하여 outputFile.tar.gz 파일을 확인한다.

실행 이력 조회

배치워크플로우 상세 내역의 하단에서 워크플로우 실행 이력을 확인할 수 있다.

워크플로우 실행 이력 화면

또한 실행이력 조회 메뉴에서 실행이력을 조회할 수 있다.

실행이력 조회 화면

배치 프로젝트 생성 및 개발

DB에서 데이터를 읽어 파일로 생성하는 간단한 Job을 만들어 테스트를 진행한다.

프로젝트 생성

  • 프로젝트 정보입력

    먼저 File -> New -> Project를 선택한 후 Chamomile Framework -> Create Chamomile Project를 선택한 후 Next를 클릭한다.

    아래와 같이 창이 생성되면 값을 입력해준다.

image-20220405164821791

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 폴더로 복사한다.

image-20220405165137173

코드 생성

  • 생성된 프로젝트 -> 마우스 오른쪽 버튼 -> Chamomile -> generate Batch Job -> OK
image-20220405165234952
  • Job 생성도구에서 다양한 배치 유형에 대해 코드를 생성할 수 있는 기능을 제공하고 있으며, 이번 예시에서는 DB to File을 선택하여 만들어 보도록 한다.

    • 배치 유형 : DB to File

    • VO 생성 방식 : 테이블에서 직접 생성

    • Job name : exportBoard

    • Package name : net.lotte.sample.board

      image-20220405165657231

  • VO 정보 입력을 위해 스키마(chamomile)테이블(demo_board)을 선택한 후 Next

    image-20220405165853799

  • Job에서 동작하는 ReaderJdbc, Stored Procedure, MyBatis 를 이용하여 처리될 수 있으며, WriterDelimited, Formatter, Json, XML 형식을 지원하며, 아래와 같이 입력 후 Finish를 선택한다.

    • Reader : JdbcCursor ItemReader

    • Reader Query : select * from demo_board

    • Writer : Json ItemWriter

      image-20220405170258816

테스트

소스코드 생성 시 배치 Job을 손쉽게 테스트 하기 위한 Junit 테스트 코드를 자동으로 생성한다.

src/test/java 폴더 내 net.lotte.sample.board.exportBoardTest를 선택하여 테스트를 수행한다.

  • 테스트는 소스코드에서 우클릭 > Run As > JUnit Test를 선택하여 실행한다.
image-20220405171308079

정상적으로 실행이 완료된 경우 demo_board 테이블의 정보가 명시 된 output 파라매터의 값에 따라 파일로 생성된 것을 확인할 수 있다.

{"docId":1,"title":"제목1","contents":"본문1","phoneNumber":"010-1111-1111"}
{"docId":2,"title":"제목2","contents":"본문2","phoneNumber":"010-2222-2222"}
{"docId":3,"title":"제목3","contents":"본문3","phoneNumber":"010-3333-3333"}
{"docId":4,"title":"제목4","contents":"본문4","phoneNumber":"010-4444-4444"}
{"docId":5,"title":"제목5","contents":"본문5","phoneNumber":"010-5555-5555"}
{"docId":10,"title":"타이블","contents":"테스트","phoneNumber":"010-9493-1326"}

서버로 배포

배치 job을 서버로 배포하기 위한 방법은 아래와 같이 2가지 방법을 제공한다.

  • class 파일 단위로 개별 배포
  • jar로 생성하여 배포 (추천)

프로젝트에서 우클릭 > Run As > 5 Maven Build...를 선택 하여 생성 된 위자드에서 Goalsclean package를 입력한 후 실행한다.

image-20220405172215114
image-20220405172215114

아래와 같이 빌드 로그가 발생하고, 프로젝트의 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 파일을 복사한다.

결과는 아래와 같다.

image-20220405172723930

각각의 배치 잡들은 별도의 프로세스로 동작하기에 재시작은 필요 없다.

워크플로우 등록 및 실행

이제 배포 된 job을 이용하여 워크플로우를 만들어 실행해 보자.

image-20220405173625725

+ 버튼을 클릭하여 새로운 배치 워크플로우를 생성한다.

image-20220405173753521

# 표시된 항목은 필수 입력 값이므로 모두 입력한다.

아이디: EXPORT_BOARD (아이디의 경우 중복 검사를 해준다.)
이름: export board workflow
스케쥴 사용여부: 미사용

+버튼을 클릭하여 Job을 생성한다.

image-20220405174721630

생성된 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 파일에 생성 된 데이터를 확인할 수 있다.

image-20220405174923215

배치 개발 가이드

File DB 공통_개발가이드

배치 개발 절차

절차

  • 배치 업무 개발은 Spring Batch를 기반으로 한다. ![image](media/배치 개발 절차.png)

배치 개발 Flow

기능 정의 (전문 정의)

  1. 배치설계서를 통해 필요한 기능을 도출한다.
  2. 배치 유형을 정의한다. (Tasklet, Job-Step)
  3. Input, Output을 정의한다. (전문정의)
  4. 선후행 작업을 정의한다.

XML 작성

  1. Job과 Step을 정의하고 관계를 작성한다.
  2. 기능정의를 통해 설계한 내용을 바탕으로 XML을 작성한다.

Java Class 작성

  1. Job을 구성하는 Step의 Class를 작성한다.

JUnit 단위 테스트

  1. Eclipse Plug-In에서 배치작업에 대한 단위테스트 코드를 생성하고 테스트를 진행한다.
  2. Junit Test Class 생성 및 단위 테스트를 진행한다.
image

배치 개발 가이드

배치 코딩 가이드 > 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과 형태는 동일하다.)
<beans:beans xmlns="http://www.springframework.org/schema/batch"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:context="http://www.springframework.org/schema/context"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans
                                 http://www.springframework.org/schema/beans/spring-beans.xsd
                                 http://www.springframework.org/schema/context
                                 http://www.springframework.org/schema/context/spring-context.xsd
                                 http://www.springframework.org/schema/batch
                                 http://www.springframework.org/schema/batch/spring-batch-2.2.xsd">
<!-- Class Path 지정한다.(자바 파일 스캔)-->
    <context:component-scan base-package="net.lotte.chamomile.batch.template.example.spring.filetofile"/>
<!-- Job 아이디 지정한다.-->
    <job id="fileTofileJob" xmlns="http://www.springframework.org/schema/batch">
        <step id="step1">
            <tasklet transaction-manager="transactionManager">
<!--Reader / Processor / Writer 함수명 매핑한다. commit-interval은 DB Commit 되는 단위 개수이다.
-->
                <chunk reader="itemReader" processor="itemProcessor" writer="itemWriter" commit-interval="2"/>
            </tasklet>
        </step>
    </job>
</beans:beans>

배치 VO클래스 작성

  • Mapping 하기 위한 VO Class를 작성한다.
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 처리 유형

  1. ItemReader

    1. Delimited

    2. FixedLength

    3. Json

    4. XML

  2. ItemProcessor

    1. ItemProcessor

    2. xmlItemProcessor

  3. ItemWriter

    1. Delimited
    2. FixedLength
    3. Json
    4. 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;
}

ItemWriter 1) 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 처리 유형

  1. ItemReader
    • JdbcCursorItemReader
    • StoredProcedureItemReader
    • JdbcPagingItemReader
    • MybatisCursorItemReader
    • MybatisPagingItemReader
  2. ItemProcessor
    • ItemProcessor
  3. 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;
}

4) MyBatisCursorItemReader

 - Mybatis Cursor로 Query를 실행하여 Record를 가져온다.
@Bean
ItemReader<ItemRecord> myBatisCursorItemReader() {
MyBatisCursorItemReader<ItemRecord> itemReader = new MyBatisCursorItemReader<>();

itemReader.setSqlSessionFactory(oracleSqlSessionFactory);
itemReader.setQueryId("selectFILETODB");/*Query ID를 설정한다.*/

return itemReader;
}

5) MyBatisPagingItemReader

- Mybatis Paging Query를 실행하여 Record를 가져온다.
@Bean
ItemReader<ItemRecord> myBatisPagingItemReader() {
final int pageSize = 10;/* 한 Page에 가져올 Row 수를 정의한다. */

MyBatisPagingItemReader<ItemRecord> itemReader = new MyBatisPagingItemReader<>();

itemReader.setSqlSessionFactory(oracleSqlSessionFactory);
itemReader.setQueryId("pagingFILETODB");/* Query ID를 설정한다. */
itemReader.setPageSize(pageSize);

return itemReader;
}

ItemWriter

1) JdbcBatchItemWriter

 - Jdbc를 사용하여 Database에 Insert 한다.
@Bean
ItemWriter<ItemRecord> jdbcBatchItemWriter() {
JdbcBatchItemWriter<ItemRecord> itemWriter = new JdbcBatchItemWriter<ItemRecord>();

itemWriter.setDataSource(dataSource);
itemWriter.setSql(
"INSERT INTO chmm_bat_exam_filetodb (id,name,email,create_dt) "
+ "VALUES (:id,:name,:email, SYSDATE)"
); /* SQL문을 설정한다. */

itemWriter.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<ItemRecord>());
return itemWriter;
}

2) JdbcXmlItemWriter

  • Jdbc를 사용하여 Database에 Insert 한다.
  • Reader가 XML 형식일 때 Target을 해당 형식으로 선언하여 사용한다.
@Bean
ItemWriter<XmlRecord> jdbcXmlItemWriter() {
JdbcBatchItemWriter<XmlRecord> itemWriter = new JdbcBatchItemWriter<XmlRecord>();

itemWriter.setDataSource(dataSource);
itemWriter.setSql(
"INSERT INTO chmm_bat_exam_filetodb (id,name,email,create_dt) "
+ "VALUES (:id,:name,:email, SYSDATE)"
);/* SQL문을 설정한다. */

itemWriter.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<XmlRecord>());
return itemWriter;
}

3) MyBatisBatchItemWriter

 - Mybatis를 사용하여 Database에 Insert 한다.
@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”

image-20201103154721682
  • JUnit Test 로그 확인
    • JUnit Test가 실행되면 Eclipse의 Console창에 로그가 출력된다.
    • 로그의 형태는 로그 세팅과 Adpater 로그에 따라 다소 차이가 있을 수 있다.

image-20201103154827312

Mybatis_개발가이드

구성도

image

배치 코딩 가이드 > Mybatis

  1. 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;
            }
    //중략..
    }
    
  2. 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>
    
  3. JOB, STEP 정의 XML 작성

    • JOB, STEP을 정의하고, Reader, Processor, Writer를 정의한다. (JOB과 STEP의 순서 정의와 배치 트랜잭션에 대한 설정을 할 수 있다.)

    • Spring Batch의 기본 기능인 트랜잭션 설정이 필요한 경우, 추가로 설정한다. (commit-interval, next 등)

    <beans:beans xmlns="http://www.springframework.org/schema/batch"
         xmlns:beans="http://www.springframework.org/schema/beans"
         xmlns:context="http://www.springframework.org/schema/context"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/context
          http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/batch
           http://www.springframework.org/schema/batch/spring-batch-2.2.                                                                 xsd">
    
        <context:component-scan base-package="net.lotte.chamomile.batch.                                             example.spring.mybatis"/>
        <job id="personJob" xmlns                       ="http://www.springframework.org/schema/batch">
     
          <step id="step1">
             <tasklet transaction-manager="transactionManager">
               <chunk reader="personReader" processor="personProcessor“
                               writer="personWriter" commit-interval="2"/>
               </tasklet>
             </step>
        </job>
    </beans:beans>
    
  4. ItemReader, ItemProcessor, ItemWriter 정의 Java 작성

    1. SqlSessionFactory

      • 한 DB에서 읽고, 다른 DB에서 쓰는 즉, 2개 이상의 DB에서의 동작을 위해서는 Spring에서 제공하는 기능인 @Autowired를 통해 특정 DB로의 접근이 가능하다. (SqlSessionFactory에 대한 설명은 3.2.1을 참고한다.)

      • 특정 DB의 접근을 위해 java 상단에 아래와 같이 사용할 각각의 DB를 정의한다.

      @Autowired
      private SqlSessionFactory oracle;
      
      @Autowired
      private SqlSessionFactory mysql;
      
      @Autowired
      Private SqlSessionFactory mssql;
      
    2. ItemReader

      • setSqlSessionFactory를 통해 특정 DB의 Connection이 가능하다. (setSqlSessionFactory의 값은 DB의 이름만 넣으면 가능하도록 SqlSessionFactory를 정의한다.)
      • SQL Query문이 들어가 있는 XML의 Query ID와 Mapping하여 특정 쿼리문을 실행시킬 수 있다.
      @Bean
      ItemReader<MybatisVO> personReader() throws Exception {
          MyBatisCursorItemReader<MybatisVO> itemReader = new MyBatisCursorItemReader<MybatisVO>();
      
          //MSSQL
          itemReader.setSqlSessionFactory(mssql);
          itemReader.setQueryId("selectUsers");
      
          return itemReader;
      }
      

배치 설정 가이드 > Mybatis

Mybatis 설정(pom.xml)

  • Mybatis 사용 Spring과 Mybatis 연결, 특정 DB와의 연결을 위해 아래와 같이 프로젝트 하위의 POM.xml에 Dependency 설정을 한다.
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>${org.mybatis.mybatis.version}</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>${org.mybatis.mybatis.mybatis-spring.version}</version>
</dependency>
<dependency>
        <groupId>com.oracle</groupId>
        <artifactId>ojdbc6</artifactId>
        <version>${com.oracle.version}</version>
</dependency>

DB Properties 설정

  • 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 등으로 정의

  • SqlSession을 관리하기 위한 SqlSessionFactory를 작성

<bean id="mssqlDatasource" class="org.apache.commons.dbcp.BasicDataSource" 
                                                      destroy-method="close">
        <property name="driverClassName" value="${mssql.jdbc.driver}" />
        <property name="url" value="${mssql.jdbc.url}" />
        <property name="username" value="${mssql.jdbc.user}" />
        <property name="password" value="${mssql.jdbc.password}" />
        … 중략 (DB 설정정보 부분) …
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource
                                .DataSourceTransactionManager" lazy-init="true">
        <property name="dataSource" ref="mssqlDatasource" />
</bean>
<bean id="mssql" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="mssqlDatasource"/>
        <property name="configLocation" 
                  value="file:conf/batch-multidb/mybatis-spring.xml" />
        <property name="mapperLocations“ value="classpath:net/lotte
                     /chamomile/batch/example/spring/mybatis/mssql/*Dao.xml" />
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg index="0" ref="mssql" />
</bean>

Junit 테스트 가이드

  • 배치업무는 단위 테스트를 위해 Junit 테스트 방법을 제공한다. 이는 개발된 업무 배치 만을 단위 테스트 한다.
  • Junit Test Class 생성
    • <모듈명>.java 생성 : 테스트에서 사용할 Class를${BATCH_HOME}/src/test/<Class Path>/<모듈명>에 생성한다.
    • Test Class를 아래와 같이 작성한다.

package net.lotte.chamomile.batch.example.spring.mybatis;

import org.junit.Test;
import net.lotte.chamomile.batch.launcher.spring.SpringJobLauncher;

public class MybatisTest {

   @Test
   public void test() throws Exception {
       try {
           /*
            * Job 및 Step이 정의되어 있는 XML 이다.
            * $personJob:테스트 Job ID 이다.
           */
           String command = "/net/lotte/chamomile/batch/example/spring/                                                 mybatis/Mybatis.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”

image-20201103154721682
  • JUnit Test 로그 확인
    • JUnit Test가 실행되면 Eclipse의 Console창에 로그가 출력된다.
    • 로그의 형태는 로그 세팅과 Adpater 로그에 따라 다소 차이가 있을 수 있다.

image-20201103154827312

에러 처리_개발가이드

배치 코딩가이드 > 에러처리

구성도

image

XML 생성

  • Job과 Step을 정의한 XML을 작성
    • 모듈에서 사용할 xml을 ${BATCH_HOME}/src/resources/jobs에 작성한다.
    • Exception을 위한 별도 XML 설정은 없으며, Class에서 처리한다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Step Class를 정의하여 명시한다.-->    
    <context:component-scan base-package="net.lotte.chamomile.batch.example.spring.errorhandling"/>
    <!--Job id를 정의한다.-->
    <job id="errorhandlingException”        xmlns="http://www.springframework.org/schema/batch">
    <!--Step을 정의한다.-->                             
    <step id="step1">
    <tasklet transaction-manager="transactionManager">
        <chunk     reader="exceptionReader"                 
        processor="exceptionProcessor" 
        writer="exceptionWriter" 
        commit-interval="2"/>
    </tasklet>
    </step>
    </job> 
</beans>

에러 처리 (Exception)

  • <모듈명>.java 생성 : 모듈에서 사용할 Class를 ${BATCH_HOME}/src/main//<모듈명>에 생성한다.
  • Exception 처리 작성 : 배치 업무 Logic 안에서 Exception 처리를 작성한다.
try {
    lineMapper.setLineTokenizer(new DelimitedLineTokenizer(","));
    lineMapper.setFieldSetMapper(new FieldSetMapper<Person>() {
        @Override
        public Person mapFieldSet(FieldSet fieldSet) throws BindException {
        Person person = new Person();
        person.setId(fieldSet.readString(0));
        person.setName(fieldSet.readString(1));
        person.setEmail(fieldSet.readString(2));
        return person;
    }
});
personReader.setLineMapper(lineMapper);
} catch (IncorrectTokenCountException e) {
    logger.error(e.toString());
    throw e;
}
  • try – catch 로 발생 가능한 Exception을 명시해 준다. 명시하지 않을 경우 발생한 Exception은 바로 상위로 전달된다.
  • 반드시 catch한 Exception을 throw하여 상위로 보낸다. 상위로 전달하지 않으면 Exception에 따른 배치 에러처리가 되지 않는다.

에러 처리 (Exit Code Mapper)

  1. 배치에서 제공하는 Exit Code Mapper 사용법은 다음과 같다.
  2. 배치처리에서 정상종료는 0, 실패는 1로 Exit Code가 설정된다.
  3. (Command Line에서 실행 시) 이를 사용자 요청에 따라 Mapping 할 수 있다.
  4. <모듈명>.xml 작성 : 모듈에서 사용할 xml을 ${BATCH_HOME}/src/resources/jobs에 작성한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="net.lotte.chamomile.batch.example.spring.errorhandling"/>

    <job id="errorHandlingExitCodeMapper" xmlns="http://www.springframework.org/schema/batch">
    <step id="step1">
    <tasklet transaction-manager="transactionManager">
        <chunk reader="exceptionReader" processor="exitCodeMapperProcessor" writer="exceptionWriter" commit-interval="2"/>
    </tasklet>
     <!--FAIL에 대한 기본 Exit Code는 FAILED이다.이를 다른 코드로 Mapping한다.예제에서는 EARLY TERMINATION으로 Mapping 한다.-->
    <fail on="FAILED" exit-code="EARLY TERMINATION"/>
    </step>
    </job>

</beans>

에러 처리 (Exit Code Mapper)

  • <모듈명>.java 생성 : 모듈에서 사용할 Class를 ${BATCH_HOME}/src/main//<모듈명>에 생성한다.
  • ExitCodeMapper 처리 작성: 대상 Class에서 ExitCodeMapper를 implements한다.
public class ErrorHandlingException implements ExitCodeMapper {

    @Override
    public int intValue(String exitCode) {
        // TODO Auto-generated method stub
        if(ExitStatus.COMPLETED.getExitCode().equals(exitCode)) {
            return 0;
        } else if(ExitStatus.FAILED.getExitCode().equals(exitCode)) {
            return 1;
        }
        //EARLY TERMINATION의 Exit Code를 3으로 설정한다.
        else if("EARLY TERMINATION".equals(exitCode)) {
            return 3;
        } else {
            return 2;
        }
    }
}

에러 처리 (Error Skip)

  • Exception 발생 시 종류 및 건수에 따라 Skip 처리할 수 있다.
  • <모듈명>.xml 작성 : 모듈에서 사용할 xml을${BATCH_HOME}/src/resources/jobs에 작성한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="net.lotte.chamomile.batch.example.spring.errorhandling"/>

    <job id=" errorhandlingErrRetry" xmlns="http://www.springframework.org/schema/batch">
    <step id="step1" allow-start-if-complete="true">
    <tasklet transaction-manager="transactionManager">
    <!-- retry-limit를 설정한다. (Retry 가능한 건 수)-->
    <chunk reader="exceptionReader" processor="errorRetryProcessor" writer="exceptionWriter" commit-interval="2" retry-limit="10">
         <!-- Retry할 Exception을 명시한다-->
         <retryable-exception-classes>
            <include class="org.springframework.dao.DeadlockLoserDataAccessException"/>
        </retryable-exception-classes>
    </chunk>
    </tasklet>
    </step>
    </job>

</beans>

에러 처리 (No Rollback)

  1. 일반적으로 (skip이나 retry가 아닐 경우) ItemWriter에서 오류가 발생 시 Rollback 처리된다.
  2. no-rollback-exception-classes 명시를 통하여 특정 Exception에서 Rollback처리를 피할 수 있다.
  3. <모듈명>.xml 작성 : 모듈에서 사용할 xml을 ${BATCH_HOME}/src/resources/jobs에 작성한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="net.lotte.chamomile.batch.example.spring.errorhandling"/>

    <job id=" errorhandlingErrRollBack" xmlns="http://www.springframework.org/schema/batch">
    <step id="step1" allow-start-if-complete="true">
    <tasklet transaction-manager="transactionManager">
    <chunk reader="exceptionReader" processor="errorRetryProcessor" writer="exceptionWriter" commit-interval="2" >
    <!-- Rollback 하지 않을 Exception을 명시한다 -->
    <no-rollback-exception-classes>
        <include class="org.springframework.batch.item.validator.ValidationException"/>
    </no-rollback-exception-classes>
    </chunk>
    </tasklet>
    </step>
    </job>

</beans>

에러 처리 (Step End)

  1. end Element를 통하여 여러 Step으로 구성된 Job의 한 Step이 FAILED 처리 되었을 경우에도 Job은 COMPLETED 처리가 가능하다.
  2. <모듈명>.xml 작성 : 모듈에서 사용할 xml을 ${BATCH_HOME}/src/resources/jobs에 작성한다.
 <bean id="normalTask" class="net.lotte.chamomile.batch.example.spring.errorhandling.NormalTasklet">
        <property name="message" value="Normal Task"/>
    </bean>

    <bean id="errorTask" class="net.lotte.chamomile.batch.example.spring.errorhandling.ErrorTasklet">
        <property name="message" value="Error Task"/>
    </bean>

    <job id="errorHandlingStepEnd" xmlns="http://www.springframework.org/schema/batch">
    <step id="step1" allow-start-if-complete="true">
    <tasklet ref="normalTask" />
    <next on="*" to="step2"/>
    </step>
    <step id="step2" allow-start-if-complete="true">
    <tasklet ref="errorTask" />
    <!-- Step Class를 정의하여 명시한다.-->
    <end on="FAILED"/>
    <next on="*" to="step3"/>
    </step>
    <step id="step3" allow-start-if-complete="true">
            <tasklet ref="normalTask" />
    </step>
    </job>

에러 처리 (Step Fail)

  1. fail Element를 통하여 여러 Step으로 구성된 Job의 한 Step이 FAILED 처리 되었을 경우에 Job을 특정 exit-code로 처리할 수 있다. (Job이 FAILED처리 되는것은 같다.)
  2. <모듈명>.xml 작성 : 모듈에서 사용할 xml을 ${BATCH_HOME}/src/resources/jobs에 작성한다.
 <bean id="normalTask" class="net.lotte.chamomile.batch.example.spring.errorhandling.NormalTasklet">
        <property name="message" value="Normal Task"/>
    </bean>

    <bean id="errorTask" class="net.lotte.chamomile.batch.example.spring.errorhandling.ErrorTasklet">
        <property name="message" value="Error Task"/>
    </bean>

    <job id="errorHandlingStepFail" xmlns="http://www.springframework.org/schema/batch">
    <step id="step1" allow-start-if-complete="true">
        <tasklet ref="normalTask" />
    <next on="*" to="step2"/>
    </step>
    <step id="step2" allow-start-if-complete="true">
        <tasklet ref="errorTask" />
        <!-- Fail 시 발생시킬 exit-code를 정의한다. 예시에서는 EARLY TERMINATION 이다.-->
        <fail on="FAILED" exit-code="EARLY TERMINATION"/>
        <next on="*" to="step3"/>
    </step>
    <step id="step3" allow-start-if-complete="true">
        <tasklet ref="normalTask" />
    </step>
    </job>

에러 처리 (Step Fail)

  1. fail Element를 통하여 여러 Step으로 구성된 Job의 한 Step이 FAILED 처리 되었을 경우에 Job을 특정 exit-code로 처리할 수 있다. (Job이 FAILED처리 되는것은 같다.)
  2. <모듈명>.xml 작성 : 모듈에서 사용할 xml을 ${BATCH_HOME}/src/resources/jobs에 작성한다.
 <bean id="normalTask" class="net.lotte.chamomile.batch.example.spring.errorhandling.NormalTasklet">
        <property name="message" value="Normal Task"/>
    </bean>

    <bean id="errorTask" class="net.lotte.chamomile.batch.example.spring.errorhandling.ErrorTasklet">
        <property name="message" value="Error Task"/>
    </bean>

    <job id="errorHandlingStepStop" xmlns="http://www.springframework.org/schema/batch" restartable="true">
    <step id="step1">
    <tasklet ref="normalTask"/>
    <!-- Stop시킬 Exit-Code를 정의한다. 예시에서는 COMPLETE 또한 STOP 이후 Job을 재시작 시 restar할 step명을 정의할 수 있다.-->
    <stop on="COMPLETED" restart="step2"/>
    </step>
    <step id="step2">
        <tasklet ref="normalTask" />
    </step>
    </job>

Batch Exception 소개

  • Spring Batch에서 발생하는 Exception의 일부를 소개한다.
    1. JobRestartException

      <job id="simpleJob" xmlns="http://www.springframework.org/schema/batch" restartable="false">
      

      Job은 기본적으로 실행 시 마다 JobInstance가 생성되어 실행되게 된다. 이러한 재실행을 방지할 필요가 있을 경우 restartable을 false로 설정할 수 있다. 그리고 이러한 Job은 재실행 시 JobRestartException을 발생하고 종료된다.

    2. FlatFileParseException FlatFileItemReader에서 File을 읽을 때 어떠한 오류가 발생한다.

    3. FlatFileFormatException LineTokenizer에서 tokenzing 시 오류가 발생한다.

    4. IncorrectTokenCountException DelimitedLineTokenizer와 FixedLengthLineTokenizer에서 FieldSet의 갯수가 맞지 않을 경우 발생한다.

    5. IncorrectLineLengthException 정의된 Range와 Line의 길이(한 행의 길이)가 같지 않을 경우에 발생한다. 단, setStrict를 통하여 오류를 넘길 수 있다.

      tokenizer.setColumns(new Range[] { new Range(1, 5), new Range(6, 10) }); tokenizer.setStrict(false);
      FieldSet tokens = tokenizer.tokenize("12345");
      assertEquals("12345", tokens.readString(0));
      assertEquals("", tokens.readString(1));
      

Junit 테스트 가이드

  • 배치업무는 단위 테스트를 위해 Junit 테스트 방법을 제공한다.
  • JUnit 테스트는 Server를 실행하지 않고 개발된 업무 배치 만을 단위 테스트 한다.

Test 배치 Class 생성

  • <모듈명>.java 생성 : 테스트에서 사용할 Class를 ${BATCH_HOME}/src/test//<모듈명>에 생성한다.
  • TEST Class 작성: 테스트에 사용할 Class를 작성한다.
package net.lotte.chamomile.batch.template.example.spring.errorhandling;

import java.util.HashMap;
import java.util.Map;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
…

import net.lotte.chamomile.batch.template.test.JobLauncherTestUtils;

@RunWith(SpringJUnit4ClassRunner.class)
//공통 설정을 가져온다. (고정값)simple-job-launcher-context.xml
//단위테스트 대상 업무의 xml을 정의한다. job-runner-context.xml
@ContextConfiguration(locations = { "/simple-job-launcher-context.xml", "/jobs/ErrorHandlingException.xml", "/job-runner-context.xml" })
public class ErrorHandlingExceptionTest {

    @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"));
        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”

image-20201103154721682
  • JUnit Test 결과 확인
    • JUnit Test가 실행되면 Eclipse의 Console창에 로그가 출력된다.
    • 로그의 형태는 로그 세팅과 Adpater 로그에 따라 다소 차이가 있을 수 있다.

image-20201103154827312

트랜잭션_개발가이드

구성도

image
  • transaction-manager : 배치 처리 중 트랜잭션의 시작과 커밋을 담당하는 역할이다.

  • commit-interval : 커밋할 아이템의 수이다.

  • Exception 발생 시 Rollback Transaction을 통해 Read된 Item이 Rollback 처리된다.

  • 각 아이템 처리 중 Skippable Exception 발생 시 Read된 Item이 삭제된 후 트랜잭션의 처음 상태로 이동한다.

설정 가이드

Job Configuration

Restartablility에 대한 XML 작성

  • “restartable (true / false)” 옵션을 사용하여 적용한다. restartable : 스프링 배치가 job를 실행할 수 있는지 여부 지정 옵션(Default : true)
    • true : Job을 1회 이상 실행 가능하다.
    • false : Job을 1회 이상 실행 불가하다. (1회 이상 실행 시, JobRestartException 발생)

<job id="personJob" xmlns=http://www.springframework.org/schema/batch                                                                   restartable="false">
  <step id="step1" next="step2">
    <tasklet transaction-manager="transactionManager" allow-start-if-complete="true">
      <chunk reader="personReader" processor="personProcessor" writer="personWriter"              commit-interval="2"/>
    </tasklet>
  </step>

  <step id="step2">
    <tasklet transaction-manager="transactionManager" allow-start-if-complete="true">
      <chunk reader="personReader" processor="personProcessor" writer="personWriter"             commit-interval="2"/>
    </tasklet>
  </step>
</job>

Job, Step, tasklet, chunk 간 포함 관계

  • Job > Step > Tasklet > Chunk

Step Configuration

Commit Interval

Commit Interval 테스트를 위한 XML

  • Listener.xml 을 이용해서 commit-interval 옵션에 대해 알아본다.
<!-- Listener.xml (commit-interval 옵션이 2일 경우)-->
<job id="personJob" xmlns="http://www.springframework.org/schema/batch">
     <step id="step1">
      <tasklet transaction-manager="transactionManager" allow-start-if-complete="true">
        <chunk reader="personReader" processor="personProcessor" writer="personWriter"                    commit-interval="2"/>
        <listeners>
           <listener ref="stepListener" />
           <listener ref="itemReaderListener" />
           <listener ref="itemWriterListener" />
        </listeners>
      </tasklet>
    </step>
</job>
...

<!--Listener.xml (commit-interval 옵션이 3일 경우)-->
<job id="personJob" xmlns="http://www.springframework.org/schema/batch">
     <step id="step1">
      <tasklet transaction-manager="transactionManager" allow-start-if-complete="true">
        <chunk reader="personReader" processor="personProcessor" writer="personWriter"                   commit-interval=“3"/>
        <listeners>
           <listener ref="stepListener" />
           <listener ref="itemReaderListener" />
           <listener ref="itemWriterListener" />
        </listeners>
      </tasklet>
    </step>
</job>

Commit Interval 결과

  • Read Listener가 2개, 혹은 3개로 달라지는 것을 볼 수 있다. (commit-interval의 이해를 돕기 위한 부분으로 Read부분만 나타낸다.)
image

Configuring Step Restart

Configuring Step Restart에 대한 XML 작성

  • “allow-start-if-complete (true / false)” 옵션을 사용하여 적용한다. (Default : False) (True : Step이 성공(complete)적으로 끝난 경우에도 해당 Step에 대해 재실행이 필요한 경우이다. False : 실패한 Step에 대해서만 재실행한다. (성공한 Step은 Skip))
<job id="personJob" xmlns="http://www.springframework.org/schema/batch">
    <step id="step1" next="step2">
    <tasklet transaction-manager="transactionManager" allow-start-if-complete="true">
    <chunk reader="personReader" processor="personProcessor" writer="personWriter" commit-interval="2"/>
    </tasklet>
    </step>

    <step id="step2">
    <tasklet transaction-manager="transactionManager" allow-start-if-complete="true">
    <chunk reader="personReader" processor="personProcessor" writer="personWriter" commit-interval="2"/>
    </tasklet>
    </step>
</job>

Controlling Rollback

ControllingRollback.java 생성

  • 에러 발생 시 선언된 특정 에러에 대해서는 에러가 발생되지 않는다. (아래 예제는 DB To DB 처리 중 Write 시 에러를 발생시키도록 하여 Rollback 예제를 구현하였다.)

    @Bean
    ItemWriter<Person> personWriter() throws Exception {
       JdbcBatchItemWriter<Person> personWriter = new JdbcBatchItemWriter<Person>();
       personWriter.setDataSource(dataSource);
       //실제 존재하지 않는 테이블에 Write하여 에러가 발생되도록 한다.
       personWriter.setSql("INSERT INTO chmm_bat_exam_dbtodb_rollback(id,name,email,inout,create_dt) VALUES                                  (:id,:name,:email,'OUT',SYSDATE)");
       personWriter.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<Person>());
       return personWriter;
    }
    
  • 에러 발생 시 선언된 특정 에러에 대해서는 에러가 발생되지 않는다. (아래 예제는 DB To DB 처리 중 Write 시 에러를 발생시키도록 하여 Rollback 예제를 구현하였다.)

    <context:component-scan base-package="net.lotte.chamomile.batch.example.spring.transaction.rollback"/>
    
    <job id="personJob" xmlns="http://www.springframework.org/schema/batch">
      <step id="step1">
        <tasklet transaction-manager="transactionManager">
          <chunk reader="personReader" processor="personProcessor" writer="personWriter" commit-interval="2"/>
            <!-- BadSqlGrammarException 발생 시 Exception을 발생시키지 않는다.-->
            <no-rollback-exception-classes>
              <include class="org.springframework.jdbc.BadSqlGrammarException"/>
            </no-rollback-exception-classes>
        </tasklet>
      </step>
    </job>
    
  • ControllingRollback 테스트 결과

    - Write 시 Write를 할 수 없다는 에러를 발생시켜야 하나
      “no-rollback-exception-classes” 사용을 통해 에러가 발생되지 않는 것을 볼 수 있다. 
      아래 결과 화면은 DB의 내용에 대한 Read만 된 후 배치가 종료된 것을 확인할 수 있다.)
    
    • 존재하지 않는 테이블명 기재로 인한 Exception 발생

      image-20201109091941221

    • 위 오류 발생 시 Exception이 발생되지 않게 “no-rollback-exception-classes” 설정 후 실행하면, 위 Exception에러가 발생되지 않는다.

      image-20201109092025292

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을 작성한다.
<!--Listener.xml-->
<!-- 생성된 리스터 매핑 정의-->
    <beans:bean id="stepListener" class="net.lotte.chamomile.batch.example.spring.dbtodb.Listener.StepListener" />
    <beans:bean id="itemReaderListener" class="net.lotte.chamomile.batch.example.spring.dbtodb.Listener.ItemReaderListener" />
    <beans:bean id="itemWriterListener" class="net.lotte.chamomile.batch.example.spring.dbtodb.Listener.ItemWriterListener" />

    <context:component-scan base-package="net.lotte.chamomile.batch.example.spring.dbtodb"/>

    <job id="personJob" xmlns="http://www.springframework.org/schema/batch">
     <step id="step1">
      <tasklet transaction-manager="transactionManager" allow-start-if-complete="true">
        <chunk reader="personReader" processor="personProcessor" writer="personWriter" commit-interval="2"/>
        <!-- 리스너 호출한다.-->  
        <listeners>
           <listener ref="stepListener" />
           <listener ref="itemReaderListener" />
           <listener ref="itemWriterListener" />
        </listeners>
      </tasklet>
     </step>
    </job>
  • Listener 테스트 결과
image-20201109092932253

Controlling Step Flow (Sequential Flow)

  • Sequential Flow 테스트를 위한 XML 작성
<!-- SequentialFlow.xml-->
<job id="personJob" xmlns="http://www.springframework.org/schema/batch">
  <step id="step1" next="step2">
    <tasklet transaction-manager="transactionManager" allow-start-if-complete="true">
      <chunk reader="personReader" processor="personProcessor" writer="personWriter" commit-interval="2"/>
    </tasklet>
  </step>

  <step id="step2">
    <tasklet transaction-manager="transactionManager" allow-start-if-complete="true">
      <chunk reader="personReader" processor="personProcessor" writer="personWriter" commit-interval="2"/>
    </tasklet>
  </step>
</job>
  • Sequential Flow 테스트 결과
image-20201109093147669

Controlling Step Flow (Conditional Flow)

  • Conditional Flow 테스트를 위한 XML 작성
conditionalFlow.xml
<job id="personJob" xmlns="http://www.springframework.org/schema/batch">
  <step id="step1">
    <tasklet transaction-manager="transactionManager" allow-start-if-complete="true">
      <chunk reader="personReader" processor="personProcessor" writer="personWriter" commit-interval="2"/>
    </tasklet>
    <!--fail 시 step 2처리한다. -->
    <next on="*" to="step3"/>
    <next on="FAILED" to="step2"/>
  </step>
  <step id="step2" next="step3">
    <tasklet transaction-manager="transactionManager" allow-start-if-complete="true">
      <chunk reader="personReader" processor="personProcessor" writer="personWriter" commit-interval="2"/>
    </tasklet>
  </step>
  <!-- Fail을 제외한 전체Step3 처리 한다.-->
  <step id="step3">
      <tasklet transaction-manager="transactionManager" allow-start-if-complete="true">
        <chunk reader="personReader" processor="personProcessor" writer="personWriter" commit-interval="2"/>
        <listeners>
          <listener ref="stepListener" />
          <listener ref="itemReaderListener" />
          <listener ref="itemWriterListener" />
        </listeners>
      </tasklet>
    </step>
</job>
  • Conditional Flow 테스트 결과

    image-20201109093523723

Junit 테스트 가이드

  • 배치업무는 단위 테스트를 위해 Junit 테스트 방법을 제공한다. 이는 개발된 업무 배치 만을 단위 테스트 한다.
  • Junit Test Class 생성
    • <모듈명>.java 생성 : 테스트에서 사용할 Class를 ${BATCH_HOME}/src/test//<모듈명>에 생성한다.
    • Test Class를 아래와 같이 작성한다.

package net.lotte.chamomile.batch.example.spring.mybatis;

import org.junit.Test;
import net.lotte.chamomile.batch.launcher.spring.SpringJobLauncher;
 
public class MybatisTest {
 
    @Test
    public void test() throws Exception {
        try {
            String command = "/net/lotte/chamomile/batch/example/spring/                                          transaction/../~.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”

image-20201103154721682
  • JUnit Test 로그 확인
    • JUnit Test가 실행되면 Eclipse의 Console창에 로그가 출력된다.
    • 로그의 형태는 로그 세팅과 Adpater 로그에 따라 다소 차이가 있을 수 있다.

image-20201103154827312

동시 실행 가능 여부 관리 기능_개발가이드

구성도

image-20201109094202121

코딩 가이드

  • XML 생성: Job과 Step을 정의한 XML을 작성한다.

    1. 기본구조 작성

      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:context="http://www.springframework.org/schema/context"
          xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd
              http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
              http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
      
      </beans>
      
    2. 상세 구조 작성: Step Class를 정의하여 명시한다. 하나의 Class로 구성된 Step일 경우 아래와 같이 component-scan을 사용한다.

        <context:component-scan base-package="net.lotte.chamomile.batch.
         template.spring.errorhandling"/>
      

      여러 개의 Class로 구성된 Step일 경우 아래과 같이 bean을 정의한다.

      <bean id="normalTask" class="net.lotte.chamomile.batch.template.spring.errorhandling.NormalTasklet">
          <property name="message" value="Normal Task"/>
      </bean>
      
    3. Job을 정의한다.

      <job id="errorHandlingStepFail“ xmlns="http://www.springframework.org/schema/batch">
      
    4. Step을 정의한다. Tasklet 구조일 경우 아래와 같이 작성한다.

      <step id="step1" allow-start-if-complete="true">
          <tasklet ref="normalTask" />
          <next on="*" to="step2"/>
      </step>
      

      Chunk 구조일 경우 아래와 같이 작성한다.

      <step id="step1">
          <tasklet transaction-manager="transactionManager">
              <chunk reader="itemReader" processor="itemProcessor" writer="itemWriter" commit-interval="2"/>
          </tasklet>
      </step>
      
  • Class 작성: XML에서 정의한 Step의 Class를 작성한다.

    1. 기본 구조 생성

      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;
      }
      
    2. 상세 구조 생성 배치 유형에 따라 상세 구조를 생성한다. (3-2 배치 기능별 가이드 참고)

    3. Restartablility 설정

      • 적용 시나리오 Job의 재기동 여부를 제한하고자 할 때 사용한다. 기본적으로 Spring Batch의 Job은 Job Parameter 별로 Job Instance가 생성되고. 실행 단위가 된다. 정상종료(COMPLETED)로실행된 Job Instance는 재실행 되지 않으나(Restartablility가 설정되어 있다 하더라도), 오류(FAILED)나 중지(STOP) 처리된 Job Instance는 재실행이 가능하다. 이러한 재실행을 방지하고자 할 때 restartable을 false로 설정한다. (기본은 true)

      • 코딩 절차 해당 Job의 restartable을 false로 작성한다.

        <job id="footballJob" restartable="false">
            ...
        </job>
        
    4. StartLimit 설정

      • 적용 시나리오 Step의 실패(FAILED) 이후 재시작 가능한 횟수를 명시하고자 할 때 start-limit를 설정할 수 있다. Limit 횟수를 초과하면org.springframework.batch.core.StartLimitExceededException이 발생한다. 기본값은 Integer.MAX_VALUE 이다.

      • 코딩 절차 해당 Step 아래 Tasklet의 start-limit를 설정한다.

        <step id="step1">
            <tasklet start-limit="1">
                <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
            </tasklet>
        </step>
        
    5. Split Flow

      • 적용 시나리오 Job을 구성하는 Step을 여러 개의 Flow(Group)으로 나눠서 구성할 수 있다. 각각의 Flow가 독립적으로 실행된다. 독립된 두개의 Step간의 구성을 하고자 할 때 사용한다. 각각의 Flow가 병렬로 실행되지는 않으며, 순차 실행된다.

        ​ 단, 상위에 기술된 Flow의 작업이 실패(FAILED) 처리 되어도 하위의 Flow에 영향을 미치지 않는다.

      • 코딩 절차 아래와 같이 Job 아래 Flow를 구성한다.

        <job xmlns="http://www.springframework.org/schema/batch" id="simultaneousSplitFlow">
           <split id="split1" next="step4">
              <flow>
                 <step id="step1" next="step2">
                    <tasklet ref="stepTask1" />
                 </step>
                 <step id="step2" next="errorStep">
                    <tasklet ref="stepTask2" />
                 </step>
                 <step id="errorStep">
                    <tasklet ref="errorTask" />
                 </step>
              </flow>
              <flow>
                 <step id="step3">
                    <tasklet ref="stepTask3" />
                 </step>
              </flow>
           </split>
           <step id="step4">
              <tasklet ref="stepTask4" />
           </step>
        </job>
        
    6. Multithreaded Step (single process)

      • 적용 시나리오 chunk로 구성된 step을 multi-thread로 처리하고자 할 때 사용한다. ​ Reader나 Writer에는 적합하지 않고, processor를 병렬로 처리하고자 할 때 사용한다.

        ​ Single-thread로 처리속도가 나오지 않을 때 multi-thread Step을 사용하면 속도가 향상된다.

        ​ 기본 thread-limit 값은 4이며, 해당 property로 조정 가능하다.

        ​ (throttle-limit) 실제 작업 수행은 task-executor가 담당하고 가장 기본적인 task-executor는

        ​ SimpleAsyncTaskExecutor이다. (7. TaskExecutor의 종류 참고)

      • 코딩 절차

        Task-executor를 정의한다.

      <bean id="taskExecutor" class="org.springframework.core.task.SimpleAsyncTaskExecutor"/>
      
      Step에 task-executor를 명시한다. (필요시 throttle-limit를 설정)
      
      <job xmlns="http://www.springframework.org/schema/batch" id="simultaneousMultithreadedStep">
         <step id="step1">
            <tasklet transaction-manager="transactionManager" task-executor="taskExecutor" throttle-limit="4">
               <chunk reader="itemReader" processor="itemProcessor" writer="itemWriter" commit-interval="4" />
            </tasklet>
         </step>
      </job> 
      

      Itemreader와 itemwriter에는 적합하지 않으나, 필요시 아래와 같이 구현 가능하다.

      <job xmlns="http://www.springframework.org/schema/batch" id="simultaneousMultithreadedStep">
         <step id="step1">
            <tasklet transaction-manager="transactionManager" task-executor="taskExecutor" throttle-limit="4">
               <chunk reader="itemReader" processor="itemProcessor" writer="itemWriter" commit-interval="4" />
            </tasklet>
         </step>
      </job>
      
    7. Parallel step (single process)

      • 적용 시나리오 plit Flow를 병렬로 처리 할 수 있다. 하나의 Job에 여러 Flow로 정의된 스텝이 있고, 상호 배타적인 관계라면, 이를 병렬로 동시에 처리하여 속도를 향상시킬 수 있다. 실제 작업 수행은 Multithreaded Step과 같이 task-executor가 담당한다.

      • 코딩 절차 아래와 같이 Split Element에 task-executor를 설정한다.

        <bean id="taskExecutor" class="org.springframework.core.task.SimpleAsyncTaskExecutor"/>
         
        <job xmlns="http://www.springframework.org/schema/batch" id="simultaneousParallelSteps" restartable="false">
           <split id="split1" task-executor="taskExecutor" next="step4">
              <flow>
                 <step id="step1" next="step2">
                    <tasklet ref="stepTask1" />
                 </step>
                 <step id="step2">
                    <tasklet ref="stepTask2" />
                 </step>
              </flow>
              <flow>
                 <step id="step3">
                    <tasklet ref="stepTask3" />
                 </step>
              </flow>
           </split>
           <step id="step4">
              <tasklet ref="stepTask4" />
           </step>
        </job>
        
    8. 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이 종료될 때 까지 대기하고, 각 결과를 취합하여 처리한다.

      • 코딩 절차 Partition Step을 구성하고, grid-size를 설정한다.

        <job xmlns="http://www.springframework.org/schema/batch" id="SimultaneousPartitioning">
           <step id="step">
              <partition step="step1" partitioner="partitioner">
                 <handler grid-size="2" task-executor="taskExecutor" />
              </partition>
           </step>
        </job>
        

        Partitioner를 정의하고, 대상 resource를 명시한다.

        <bean id="partitioner" class="org.springframework.batch.core.partition.support.MultiResourcePartitioner">
           <property name="resources" value="classpath:data/iosample/input/delimited*.csv" />
        </bean>
        

        Task Executor를 정의한다.

        <bean id="taskExecutor" class="org.springframework.core.task.SimpleAsyncTaskExecutor"/>
        

        대상 Step을 작성한다.

        <step xmlns="http://www.springframework.org/schema/batch" id="step1">
           <tasklet transaction-manager="transactionManager">
              <chunk writer="itemWriter" reader="itemReader" processor="itemProcessor" commit-interval="5" />
              <listeners>
                 <listener ref="fileNameListener" />
              </listeners>
           </tasklet>
        </step>
        

        전체 구조는 아래와 같다

        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd   http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd   http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
           <job xmlns="http://www.springframework.org/schema/batch" id="SimultaneousPartitioning">
              <step id="step">
                 <partition step="step1" partitioner="partitioner">
                    <handler grid-size="2" task-executor="taskExecutor" />
                 </partition>
              </step>
           </job>
           <bean id="partitioner" class="org.springframework.batch.core.partition.support.MultiResourcePartitioner">
              <property name="resources" value="classpath:data/iosample/input/delimited*.csv" />
           </bean>
        <bean id="taskExecutor" class="org.springframework.core.task.SimpleAsyncTaskExecutor" />
           <step xmlns="http://www.springframework.org/schema/batch" id="step1">
              <tasklet transaction-manager="transactionManager">
                 <chunk writer="itemWriter" reader="itemReader" processor="itemProcessor" commit-interval="5" />
                 <listeners>
                    <listener ref="fileNameListener" />
                 </listeners>
              </tasklet>
           </step>
           <bean id="fileNameListener" class="net.lotte.chamomile.batch.common.OutputFileListener" scope="step">
              <property name="path" value="file:./build/output/file/" />
           </bean>
           <bean id="itemReader" scope="step" autowire-candidate="false" parent="itemReaderParent">
              <property name="resource" value="#{stepExecutionContext[fileName]}" />
           </bean>
           <!-- 
            <bean id="inputTestReader" class="org.springframework.batch.item.file.MultiResourceItemReader">
                <property name="resources" value="classpath:data/iosample/input/delimited*.csv" />
                <property name="delegate" ref="testItemReader" />
            </bean>
            <bean id="outputTestReader" class="org.springframework.batch.item.file.MultiResourceItemReader" scope="prototype">
                <property name="resources" value="file:build/output/file/delimited*.csv" />
                <property name="delegate" ref="testItemReader" />
            </bean>
         
            <bean id="testItemReader" parent="itemReaderParent" />-->
           <bean id="itemReaderParent" class="org.springframework.batch.item.file.FlatFileItemReader" abstract="true">
              <property name="lineMapper">
                 <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                    <property name="lineTokenizer">
                       <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
                          <property name="delimiter" value="," />
                          <property name="names" value="name,credit" />
                       </bean>
                    </property>
                    <property name="fieldSetMapper">
                       <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
                          <property name="targetType" value="net.lotte.chamomile.batch.domain.trade.CustomerCredit" />
                       </bean>
                    </property>
                 </bean>
        </bean>
           <bean id="itemProcessor" class="net.lotte.chamomile.batch.domain.trade.internal.CustomerCreditIncreaseProcessor" />
           <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step">
              <property name="resource" value="#{stepExecutionContext[outputFile]}" />
              <property name="lineAggregator">
                 <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
                    <property name="delimiter" value="," />
                    <property name="fieldExtractor">
                       <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
                          <property name="names" value="name,credit" />
                       </bean>
                    </property>
                 </bean>
              </property>
           </bean>
        </beans>
         
        
    9. Partitioning Step 2 (single process or multiprocess: DB to File)

      • 적용 시나리오 입력이 DB일 때, File to File과 동일한 경우에 사용한다.

      • 코딩 절차 File to File가 동일한 절차로 진행한다. 전체 소스는 아래와 같다.

        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd   http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
           <job xmlns="http://www.springframework.org/schema/batch" id="simultaneousPartitionJdbcJob">
              <step id="step">
                 <partition step="step1" partitioner="partitioner">
                    <handler grid-size="${batch.grid.size}" task-executor="taskExecutor" />
                 </partition>
              </step>
           </job>
           <bean id="partitioner" class="net.lotte.chamomile.batch.common.ColumnRangePartitioner">
              <property name="dataSource" ref="dataSource" />
              <property name="table" value="EMP" />
              <property name="column" value="EMPNO" />
           </bean>
           <bean id="taskExecutor" class="org.springframework.core.task.SimpleAsyncTaskExecutor" />
           <step xmlns="http://www.springframework.org/schema/batch" id="step1">
        <chunk writer="itemWriter" reader="itemReader" processor="itemProcessor" commit-interval="5" />
                 <listeners>
                    <listener ref="fileNameListener" />
                 </listeners>
              </tasklet>
           </step>
           <bean id="fileNameListener" class="net.lotte.chamomile.batch.common.OutputFileListener" scope="step">
              <property name="path" value="file:./build/output/jdbc/" />
           </bean>
           <bean id="itemReader" scope="step" autowire-candidate="false" class="org.springframework.batch.item.database.JdbcPagingItemReader">
              <property name="dataSource" ref="dataSource" />
              <property name="rowMapper">
                 <bean class="net.lotte.chamomile.batch.domain.trade.internal.EmpSalRowMapper" />
              </property>
              <property name="queryProvider">
                 <bean class="org.springframework.batch.item.database.support.SqlPagingQueryProviderFactoryBean">
                    <property name="dataSource" ref="dataSource" />
                    <property name="fromClause" value="ORACLE.EMP" />
                    <property name="selectClause" value="EMPNO,ENAME,SAL" />
                    <property name="sortKeys">
                       <map>
                          <entry key="EMPNO" value="ASCENDING" />
                       </map>
                    </property>
                    <property name="whereClause" value="EMPNO &gt;= :minEmpno and EMPNO &lt;= :maxEmpno" />
                 </bean>
              </property>
              <property name="parameterValues">
                 <map>
                    <entry key="minEmpno" value="#{stepExecutionContext[minValue]}" />
                    <entry key="maxEmpno" value="#{stepExecutionContext[maxValue]}" />
                 </map>
              </property>
           </bean>
           <bean id="outputTestReader" class="org.springframework.batch.item.file.MultiResourceItemReader" scope="prototype">
              <property name="resources" value="file:build/output/jdbc/*.csv" />
              <property name="delegate" ref="testItemReader" />
           </bean>
           <bean id="testItemReader" class="org.springframework.batch.item.file.FlatFileItemReader">
              <property name="lineMapper">
                 <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                    <property name="lineTokenizer">
                       <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
                          <property name="delimiter" value="," />
                          <property name="names" value="empno,ename,sal" />
                       </bean>
        </property>
                    <property name="fieldSetMapper">
                       <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
                          <property name="targetType" value="net.lotte.chamomile.batch.domain.trade.EmpSal" />
                       </bean>
                    </property>
                 </bean>
              </property>
           </bean>
           <bean id="inputTestReader" class="org.springframework.batch.item.database.JdbcCursorItemReader">
              <property name="sql">
                 <value><![CDATA[select EMPNO,ENAME,SAL from EMP]]></value>
              </property>
              <property name="dataSource" ref="dataSource" />
              <property name="verifyCursorPosition" value="${batch.verify.cursor.position}" />
              <property name="rowMapper">
                 <bean class="net.lotte.chamomile.batch.domain.trade.internal.EmpSalRowMapper" />
              </property>
           </bean>
             <bean id="itemProcessor" class="net.lotte.chamomile.batch.domain.trade.internal.EmpSalIncreaseProcessor" />
           <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step">
              <property name="resource" value="#{stepExecutionContext[outputFile]}" />
              <property name="lineAggregator">
                 <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
                    <property name="delimiter" value="," />
                    <property name="fieldExtractor">
                       <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
                          <property name="names" value="empno,ename,sal" />
                       </bean>
                    </property>
                 </bean>
              </property>
           </bean>
        </beans>
        
    10. TaskExecutor의 종류

      • 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”

image-20201103154721682

JUnit Test 로그 확인

  • JUnit Test가 실행되면 Eclipse의 Console창에 로그가 출력된다.
  • 로그의 형태는 로그 세팅과 Adpater 로그에 따라 다소 차이가 있을 수 있다.
image-20201109102956564
다음 Batch 설치 가이드