> For the complete documentation index, see [llms.txt](https://tech.x2bee.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tech.x2bee.com/dev-guide/pjt-prepare/publish-your-docs/batch.md).

# Batch 개발 가이드

본 문서에서는 X2BEE 배치 시스템 구성 및 Spring Boot Batch에 대해 설명합니다.

먼저, Cronicle 스케줄러의 작성 방법과 소스 코드 작성 절차를 설명하고, 이를 토대로 간단한 배치 작업을 수행하는 샘플 프로그램 목록을 제공합니다.

***

## X2BEE 배치 시스템 구성

다음은 배치 시스템 전체 구성도 입니다.

<figure><img src="/files/68jAMbh01ybWcQmnoRE1" alt=""><figcaption></figcaption></figure>

<table><thead><tr><th width="136">구분</th><th>설명</th></tr></thead><tbody><tr><td>배치서비스</td><td>배치 업무를 수행하는 프로그램입니다. 스케줄러에 의해 정해진 주기로 기능이 실행되거나 즉시 실행될 수 있습니다. 데이터 저장소의 데이터를 조회하여 처리 후 다시 데이터 저장소에 저장하는 것이 주요 업무입니다. Spring batch를 사용하여 구현되며, Web 서비스로 동작하여 HTTP 요청을 통해 배치 잡(job)을 실행할 수 있습니다.</td></tr><tr><td>스케줄러</td><td>배치 잡(job)과 그 실행주기를 등록하고 관리합니다. 실행 주기가 되었을 때 배치 잡이 실행되도록 배치 서비스에 HTTP 요청을 보내고, 필요에 따라 즉시 실행 기능을 제공합니다. 실행 결과 로그를 남기고 실행 결과 통계를 UI를 통해서 제공합니다. 공개소프트웨어를 사용합니다.</td></tr><tr><td>BO배치관리</td><td>필요에 따라 보다 세부적인 배치 관리 기능을 BO 시스템에 커스텀으로 구현합니다. 데이터 처리 및 통계 및 즉시 중지 기능 등을 제공합니다.</td></tr><tr><td>데이터저장소</td><td>대상 데이터는 제한이 없으며 DB, File, S3, Queue 등을 사용할 수 있습니다.</td></tr></tbody></table>

***

## Spring boot batch 설명

배치 서비스는 Spring Batch를 사용하여 개발합니다.

Spring Batch는 로깅/추적, 트랜잭션 관리, 작업 처리 통계, 작업 재시작, 건너뛰기 및 리소스 관리와 같이 대량의 레코드를 처리하는 데 필요한 재사용 가능한 핵심 기능을 제공합니다. 또한 최적화 및 파티셔닝 기술을 활용하여 고성능 배치 작업을 효과적으로 수행할 수 있는 고급 기술과 기능을 제공합니다.

### Spring Batch 프로그램 구조

Spring Batch 프로그램 구조는 Tasklet 기반과 Chunk 기반으로 나뉩니다.

**Chunk 기반**

* 한 번에 고정된 양의 레코드(Chunk)를 읽고 처리하는 방식입니다.
* Chunk 단위로 트랜잭션을 수행하기 때문에 실패 시 해당 Chunk만 롤백됩니다.
* 페이징 크기와 커밋 간격(Chunk Size)을 일치시키는 것이 권장됩니다.
* 사용 예: 대량 데이터 변경 작업

중요 개념:

* Job, Step: 작업의 최소 단위. 하나의 Job은 하나 이상의 Step으로 구성됩니다.
* Chunk: 트랜잭션의 관리 단위.
* reader/processor/writer: 데이터를 읽고/가공하고/저장하는 컴포넌트

<figure><img src="/files/FGgbnBdKQwjzN3240IfZ" alt=""><figcaption></figcaption></figure>

Tasklet 기반

* 단일 태스크를 수행하는 방식입니다. execute 메서드를 반복 호출하여 작업을 수행합니다.
* 초기화, 저장 프로시저 실행, 알림 전송 등에서 사용됩니다.
* 간단한 배치는 Tasklet으로 쉽게 구현되지만, 대량 처리는 복잡해질 수 있습니다.

<figure><img src="/files/XZ0jikAkLOHT3DMpCW6R" alt=""><figcaption></figcaption></figure>

***

## cronicle 설명

cronicle은 웹 기반 프런트 엔드 UI를 갖춘 다중 서버 작업 스케줄러 및 실행기입니다. 실시간 통계 및 라이브 로그 뷰어를 통해 여러 슬레이브 서버를 대상으로 예약된 작업, 반복 작업 및 주문형 작업을 처리합니다.

기능 요약:

* 단일 또는 다중 서버 설정
* 백업 서버에 대한 자동 장애 조치
* 근접 서버 자동 검색
* 라이브 로그 뷰어
* 다양한 시간대에 이벤트 예약
* 이벤트 대기열 관리(옵션)
* CPU 및 메모리 사용량 추적
* 과거 통계 및 성능 그래프
* 웹후크 지원
* 이벤트 예약 및 실행을 위한 REST API

### cronicle 이벤트 작성

로그인 후 Schedule 탭 하단의 Add Event를 통해 이벤트를 생성합니다.

<figure><img src="/files/km0uxSgyEFkvMpZqaQyN" alt=""><figcaption></figcaption></figure>

주요 속성:

* Event Name: 이벤트 이름
* Category: 이벤트 카테고리
* Plugin: 실행될 플러그인
* Target: 실행 서버 대상
* Timing: 실행 주기(일일, 시간별 등)

<figure><img src="/files/I1KN6OiCVXifqHnb7Vyw" alt=""><figcaption></figcaption></figure>

타이밍은 시각적 다중 선택기 위젯을 사용하여 다양한 날짜/시간 예약이 가능합니다. Crontab 설정을 사용하려면 \[ Import… ] 링크를 통해 입력할 수 있습니다.

***

## 컨트롤러 작성

HTTP 요청을 통해 배치 Job을 기동하는 데 사용됩니다. 공통 Controller(BatchJobController)를 사용하면 별도 컨트롤러를 작성하지 않아도 됩니다. 별도의 기능이 필요한 경우에만 컨트롤러를 작성합니다.

### 클래스 어노테이션

클래스 레벨에 다음 어노테이션을 사용합니다.

<table><thead><tr><th width="250.6666259765625">어노테이션</th><th>설명</th></tr></thead><tbody><tr><td>@RestController</td><td>REST 컨트롤러임을 표시 (@ResponseBody + @Controller 역할)</td></tr><tr><td>@RequestMapping</td><td>컨트롤러 클래스 수준의 공통 URI 지정</td></tr><tr><td>@Slf4j</td><td>lombok 로그 어노테이션</td></tr><tr><td>@RequiredArgsConstructor</td><td>lombok 생성자 주입 편의 어노테이션</td></tr></tbody></table>

예시:

```java
@RestController
@RequestMapping("/samples")
@Slf4j
@RequiredArgsConstructor
public class SampleParamJobController { ... }
```

### 매핑 URI 형식

@RequestMapping의 path 파라미터 형식: /batch/\<mbod,gddp>/\<resource명 복수형>/<배치Job명>

* /batch/\<mbod,gddp>: context-path로 지정 (프로그램에서 지정하지 않음)
* resource명 복수형: 클래스 레벨 @RequestMapping에서 지정
* 배치Job명: resource명을 Job명 앞에 붙이는 형식

예:

* context-path: /batch/gddp
* 클래스 레벨 RequestMapping: @RequestMapping("/categories")
* 개별 매핑 예:
  * @GetMapping("/categoryRankPopularJob")
  * @GetMapping("/categoryRankPopularGoodsJob")

### 메소드 파라미터 어노테이션

| 어노테이션         | 설명                                  |
| ------------- | ----------------------------------- |
| @PathVariable | JobName은 PathVariable 형식을 사용        |
| @RequestParam | 파라미터는 QueryString 또는 FormData 방식 사용 |

예시 시그니처:

```java
@GetMapping("/{jobName}")
public String sampleJobs(@PathVariable String jobName,
                         @RequestParam MultiValueMap<String, String> parameters) throws Exception { ... }
```

### 기본 소스코드 (예시)

```java
public class SampleParamJobController {
    private final JobLauncher jobLauncher;
    private final BatchCommonService batchCommonService;
    // job 선언, DI
    private final Job sampleParamJob;

    @GetMapping("/sampleParamJob")
    public String sampleParamJob(@RequestParam MultiValueMap<String, String> parameters) throws Exception {
        // 파라미터 준비 + increment(중복실행허용)
        JobParameters jobParameters = batchCommonService.setIncrementer(sampleParamJob);
        jobParameters = new JobParametersBuilder(jobParameters)
            .addString("sampleParam", parameters.getFirst("sampleParam"))
            .toJobParameters();

        // 실행
        JobExecution jobExecution = jobLauncher.run(sampleParamJob, jobParameters);
        Log.info("Batch job has been invoked: {}", jobExecution);

        // 실행성공
        return "Batch job has been invoked";
    }
}
```

아래는 위 예시의 주요 단계들을 stepper로 정리한 내용입니다.

{% stepper %}
{% step %}

### Job 멤버 선언 및 DI

* Job을 클래스 멤버로 선언하고 의존성 주입(DI) 처리합니다.
  {% endstep %}

{% step %}

### 중복 실행 허용 및 파라미터 준비

* incrementer를 통해 중복 실행 관리.
* JobParametersBuilder로 필요한 파라미터를 추가합니다.
  {% endstep %}

{% step %}

### Job 실행

* JobLauncher.run(job, jobParameters)으로 Job을 실행합니다.
* 실행 결과는 Spring Batch repository에서 확인합니다.
  {% endstep %}
  {% endstepper %}

### 메소드 리턴값

컨트롤러의 반환값은 단순 String을 사용합니다. 호출 결과만 리턴하고, 상세 실행 결과는 Spring Batch repository에서 확인합니다.

***

## Job Configuration 클래스 작성

Job Configuration 클래스는 Spring Batch에서 Job, Step, Reader/Writer/Processor 등의 Bean을 정의하는 클래스입니다.

요약:

* Java 17 이상에서 지원
* 기존의 StepBuilderFactory, JobBuilderFactory는 더 이상 권장되지 않음. 대신 JobRepository와 PlatformTransactionManager를 명시적으로 사용
* @EnableBatchProcessing은 더 이상 사용하지 않아도 됨(권장하지 않음)

예시는 CompositeItemWriter 방식으로 작성되었습니다.

### 클래스 레벨 어노테이션

| 어노테이션                    | 설명                   |
| ------------------------ | -------------------- |
| @Configuration           | Job, Step 등 Bean 선언용 |
| @Slf4j                   | lombok 로그            |
| @RequiredArgsConstructor | lombok 생성자 주입 편의     |

예시:

```java
@Configuration
@RequiredArgsConstructor
@Slf4j
public class SimpleJobConfig {
    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    ...
}
```

### Job 생성 (예시)

```java
@Bean
public Job simpleComposItemWriterJob() {
    return new JobBuilder("simpleComposItemWriterJob", jobRepository)
        .start(simpleComposItemWriterStep()) // Step 설정
        .incrementer(new UniqueRunIdIncrementer()) // 중복실행허용
        .build();
}
```

설명:

* 일반적으로 하나의 Step을 구성하고 필요시 nextStep을 추가
* incrementer로 중복 실행 관리 가능
* JobParametersValidator를 사용해 실행 파라미터 검증 가능

### Step 생성 (Chunk 기반 예시)

```java
@Bean
public Step simpleComposItemWriterStep() {
    return new StepBuilder("simpleComposItemWriterStep", jobRepository)
        .<SampleRequest, SampleRequest>chunk(CHUNK_SIZE, transactionManager)
        .reader(simpleItemReader())
        // .processor(...)
        .writer(simpleCompositeItemWriter())
        .build();
}
```

* reader: 데이터를 조회
* processor: 필요시 데이터 가공(선택)
* writer: 처리된 데이터를 저장(Insert/Update/Send 등)
* chunk 및 transactionManager를 통해 트랜잭션 단위를 지정

Chunk 기반의 reader/processor/writer 예시:

```java
@Bean
@StepScope
public ItemReader<SampleRequest> simpleItemReader() {
    return new ListItemReader<>(batSampleCompositeService.reader());
}

@Bean
public ItemWriter<SampleRequest> simpleCompositeItemWriter() {
    CompositeItemWriter<SampleRequest> compositeItemWriter = new CompositeItemWriter<>();
    compositeItemWriter.setDelegates(Arrays.asList(simpleUpdateService() /*, sampleUpdateService2() */));
    return compositeItemWriter;
}

@Bean
public ItemWriter<SampleRequest> simpleUpdateService() {
    return sampleList -> sampleList.forEach(batSampleCompositeService::writer2);
}
```

Tasklet 기반의 Step 작성 예시:

```java
@Bean
@JobScope
public Step sampleParamStep() {
    return new StepBuilder("sampleParamStep", jobRepository)
        .start(sampleParamTasklet(null))
        .incrementer(new UniqueRunIdIncrementer())
        .build();
}

@Bean
@JobScope
public SampleParamTasklet sampleParamTasklet(@Value("#{jobParameters[sampleParam]}") String sampleParam) {
    return new SampleParamTasklet(sampleParam);
}
```

Tasklet 구현 예시:

```java
public class SampleParamTasklet implements Tasklet {
    @Autowired
    private SampleService sampleService;
    private String sampleParam;

    public SampleParamTasklet(String sampleParam) {
        this.sampleParam = sampleParam;
    }

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        List<Sample> list = sampleService.getSampleList(new Sample());
        for (Sample sample : list) {
            Log.info("!!!!!! executed tasklet !!!!!!: {}, sampleParam: {}", sample, sampleParam);
        }
        return RepeatStatus.FINISHED;
    }
}
```

***

## 샘플 프로그램 목록

배치 프로젝트(gddp)의 샘플 패키지에 포함된 샘플 목록과 설명:

| 배치프로그램명                   | 파일                                  | 설명                                                                        |
| ------------------------- | ----------------------------------- | ------------------------------------------------------------------------- |
| simpleComposItemWriterJob | SimpleJobConfig.java                | CompositeItemWriter를 사용한 reader/writer 2단계 구성, processor 생략 샘플            |
| sampleFileJob             | SampleFileJobConfig.java            | FlatFileItemReader를 이용한 CSV 파일 읽기 샘플                                      |
| sampleCompositeWriterJob  | SampleCompositeWriterJobConfig.java | CompositeItemWriter로 Insert/Update/Send 등의 복합 작업 처리 샘플                    |
| sampleMyBatisCursorJob    | SampleMyBatisCursorJobConfig.java   | MyBatis Cursor Reader와 MyBatis Batch Writer 사용 샘플 (ParameterConverter 포함) |
| sampleJdbcJob             | SampleJdbcJob.java                  | JdbcCursorItemReaderBuilder와 JdbcBatchItemWriterBuilder 사용 샘플             |
| sampleJdbcPagingJob       | SampleJdbcPagingConfig.java         | JdbcPagingItemReaderBuilder 사용 샘플                                         |

간단한 일부 샘플 설명:

simpleComposItemWriterJob

* CompositeItemWriter를 사용하여 여러 writer를 Delegates로 구성.

sampleFileJob

* FlatFileItemReader로 CSV 파일을 읽음.

```java
@Bean
@StepScope
public FlatFileItemReader<SampleFileRequest> sampleFileReader() {
    String[] names = new String[] {"name", "description"};
    return new FlatFileItemReaderBuilder<SampleFileRequest>()
        .name("sampleFileRequest")
        .resource(new ClassPathResource("/csv/sample_data.csv"))
        .linesToSkip(1)
        .targetType(SampleFileRequest.class)
        .delimited().delimiter(",")
        .names(names)
        .build();
}
```

sampleCompositeWriterJob

* Processor를 포함하여 Input 타입(SampleRequest) → Output 타입(SampleResponse)으로 변환하는 샘플.

sampleMyBatisCursorJob

* MyBatis Cursor Reader와 MyBatis Batch Writer 사용 예시:

```java
@Bean
public MyBatisCursorItemReader<SampleRequest> sampleMyBatisCursorItemReader() {
    Map<String, Object> parameterValues = new HashMap<>();
    return new MyBatisCursorItemReaderBuilder<SampleRequest>()
        .sqlSessionFactory(displayRodbSqlSessionFactory)
        .queryId("com.x2bee.batch.gddp.app.repository.displayrodb.sample.BatSampleMapper.selectSampleList")
        .parameterValues(parameterValues)
        .build();
}

@Bean
public ItemWriter<SampleRequest> sampleMyBatisBatchItemWriter() {
    return new MyBatisBatchItemWriterBuilder<SampleRequest>()
        .sqlSessionFactory(displayRwdbSqlSessionFactory)
        .assertUpdates(false)
        .itemToParameterConverter(item -> {
            Map<String, Object> parameter = new HashMap<>();
            parameter.put("sysModrId", "BATCH");
            parameter.put("name", item.getName());
            return parameter;
        })
        .statementId("com.x2bee.batch.gddp.app.repository.displayrwdb.sample.BatSampleTrxMapper.updateSample")
        .build();
}
```

sampleJdbcJob (JdbcCursor, JdbcBatch 예시)

* JdbcCursorItemReaderBuilder 및 JdbcBatchItemWriterBuilder 사용. BoundSql에서 SQL을 가져와 PreparedStatementSetter 설정 등.

sampleJdbcPagingJob

* JdbcPagingItemReaderBuilder와 SqlPagingQueryProviderFactoryBean을 이용한 페이징 처리 예시.

***

## Spring Boot Batch 성능을 최적화하기 위한 Reader

Reader는 데이터를 효율적으로 읽어오고 처리 속도를 최적화하도록 구성해야 합니다. 아래는 주요 Reader 및 특징입니다.

* JdbcPagingItemReader
  * 페이지별로 데이터를 읽어 메모리 사용 효율적
  * 뒤로 갈수록 성능 저하 발생 가능
* JdbcCursorItemReader
  * 커서 기반으로 필요 시마다 데이터를 읽음
  * 메모리 효율적이며 대량 데이터 처리에 적합
* MyBatisPagingItemReader
  * MyBatis를 사용한 페이징 Reader
* MyBatisCursorItemReader
  * MyBatis와 커서 기반 통합 Reader

### 커스텀 Reader: QuerydslNoOffsetPagingItemReader

* QueryDSL을 사용해 offset 없이 페이징(Zero-Offset) 처리
* 장점: offset 성능 이슈 해결, 빠른 페이징
* 단점: Spring Batch에서 공식 지원하지 않음, 복잡한 쿼리에는 비권장

예시(요약):

```java
@Bean
@StepScope
public QuerydslNoOffsetPagingItemReader<QuerydslEntity> sampleQuerydslNoOffsetPaginItemReader(@Value("#{jobParameters[strDtm]}") String strDtm) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd HH:mm:ss");
    LocalDateTime dateTime = LocalDateTime.parse(strDtm, formatter);

    QuerydslNoOffsetStringOptions<QuerydslEntity> options =
        new QuerydslNoOffsetStringOptions<>(querydslEntity.mbrNo, QuerydslReaderExpression.ASC);

    return new QuerydslNoOffsetPagingItemReader<>(entityManagerFactory, CHUNK_SIZE, options, queryFactory ->
        queryFactory.selectFrom(querydslEntity)
            .where(querydslEntity.mbrJoinDtm.after(dateTime))
    );
}
```

***

## 실제 예제와 테스트 (요약)

테스트 대상:

* 데이터 총 건수: 200k, 700k, 1,000k 건
* Chunk 크기: 1000 (모든 테스트에서 동일)
* 테스트 환경: 12th Gen Intel Core i7-1260P / LPDDR5 16GB
* 모든 ItemWriter는 동일하게 MyBatisBatchItemWriter 혹은 JdbcBatchItemWriter 사용

테스트 결과 요약(문서의 상세 표를 참조):

* Tasklet 기반: 가장 빠른 성능을 보였으나 메모리 사용량이 가장 높음(OOM 가능성)
* Chunk 기반: Tasklet보다 메모리 사용량 적음(Chunk 단위 트랜잭션)
* MyBatisCursor / JdbcCursor: 초기 Cursor Open 비용 존재, 이후 좋은 처리 성능
* JdbcPaging: 초반에는 빠르나 뒤로 갈수록 느려짐(Offset 증가 영향)
* QuerydslNoOffset: 테스트에서 가장 좋은 성능을 보였음(조건에 따라 유리)

(원문 문서에 상세한 성능 표가 포함되어 있습니다.)

***

## 최종결론

권장 조합(요약):

| Reader / Writer                                       | 내용                                                                                              |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| MyBatisCursorItemReader / MybatisBatchItemWriter      | 일반 환경에서는 MyBatis Cursor가 빠름. 저장은 MybatisBatchItemWriter로 Batch Insert/Update 권장.                |
| MyBatisPagingItemReader / MybatisBatchItemWriter      | 페이징을 항상 첫 페이지(offset 0)만 조회하도록 구성할 수 있다면 메모리 사용 적고 빠름(비즈니스 로직에 따라 유용).                          |
| QueryDslZeroOffsetItemReader / MybatisBatchItemWriter | 단일 테이블, PK가 시퀀스 형태인 경우 QueryDSL 기반 Zero-Offset Reader가 매우 우수한 성능을 보임. 단점은 쿼리를 QueryDSL로 변경해야 함. |
