# Spring Batch 직접 구현하기 (2)

{% hint style="info" %}
새로운 요구사항을 적용하면서 Spring Batch 에서 Step 을 구성하는 이유를 알아본다. (Job 에서 Step 을 추가로 구성하는 이유)
{% endhint %}

## 유스케이스 : 휴면 상태 전환을 휴면 대상 고객에게 1주일 전에 안내 메일을 보낸다.

[Spring Batch 직접 구현하기 (1)](/second-brain/technical/study/dont-reinvent-the-wheel/spring-batch/spring-batch-1.md) 에서 유스케이스가 추가되었다.

1. 휴면 전환 1 주일 전인 고객에게 로그인하지 않으면 휴면 전환 된다는 안내 메일 보내기 (추가 요구사항)
2. 당일 기준 휴면 전환 고객은 휴면 상태로 전환한다. (기존 요구사항)

어떻게 구현하는 것이 좋을까?

<div align="center"><figure><img src="/files/U6uwPUJ4qRGRMWd4ftsK" alt="" width="467"><figcaption></figcaption></figure></div>

## 리팩토링 목표

<figure><img src="/files/ZS2BGFFxHmWG2AwbbcfU" alt="" width="457"><figcaption></figcaption></figure>

추가 요구사항을 Step 으로 구현한다.

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

### Step

* 하나의 Job 은 한개 이상의 Step 으로 구성된다.
* 각각의 Step 은 서로 독립적이다.
* Step 은 순서를 가지고 있고 순차적으로 실행된다. (ArrayList 로 Job 이 Step 을 가지고 있고 순차적으로 실행함)

## Before

```java
@Configuration
public class DormantBatchConfiguration {

    @Bean
    public Job dormantBatchJob(
            DormantBatchItemReader itemReader,
            DormantBatchItemProcessor itemProcessor,
            DormantBatchItemWriter itemWriter,
            DormantBatchJobExecutionListener listener
    ) {
        return Job.builder()
                .itemReader(itemReader)
                .itemProcessor(itemProcessor)
                .itemWriter(itemWriter)
                .jobExecutionListener(listener)
                .build();
    }
}
```

```java
public class Job {

    private final Tasklet tasklet;
    private final JobExecutionListener jobExecutionListener;

    public JobExecution execute() {

        final JobExecution jobExecution = new JobExecution();
        jobExecution.setStatus(BatchStatus.STARTING);
        jobExecution.setStartTime(LocalDateTime.now());

        jobExecutionListener.beforeJob(jobExecution);

        try {
            tasklet.execute();
            jobExecution.setStatus(BatchStatus.COMPLETED);
        } catch (Exception e) {
            jobExecution.setStatus(BatchStatus.FAILED);
        }
        jobExecution.setEndTime(LocalDateTime.now());

        jobExecutionListener.afterJob(jobExecution);

        return jobExecution;
    }
}
```

클라이언트 코드

```java
@Autowired
private Job dormantBatchJob;

@Test
void test2() {
    final JobExecution result = dormantBatchJob.execute();
}
```

## After

### Step 1. Step 을 가지고 있는 Job 을 구현한다.

`Job` 을 인터페이스로 분리한다.

```java
public interface Job {
    JobExecution execute();
}
```

`doExecute()` 를 추상메서드로 하는 `Job` 템플릿 코드를 추상 클래스로 구현한다.

```java
public abstract class AbstractJob implements Job {

    private final JobExecutionListener jobExecutionListener;

    @Override
    public JobExecution execute() {
        final JobExecution jobExecution = new JobExecution();
        jobExecution.setStatus(BatchStatus.STARTING);
        jobExecution.setStartTime(LocalDateTime.now());

        jobExecutionListener.beforeJob(jobExecution);

        try {
            doExecute();
            jobExecution.setStatus(BatchStatus.COMPLETED);
        } catch (Exception e) {
            jobExecution.setStatus(BatchStatus.FAILED);
        }

        jobExecution.setEndTime(LocalDateTime.now());

        jobExecutionListener.afterJob(jobExecution);

        return jobExecution;
    }

    public abstract void doExecute();

}
```

`Step` 을 가지고 있는 `Job` 을 구현한다.

```java
public class StepJob extends AbstractJob {

    private final List<Step> steps;

    public StepJob(List<Step> steps, JobExecutionListener jobExecutionListener) {
        super(jobExecutionListener);
        this.steps = steps;
    }

    @Override
    public void doExecute() {
        steps.forEach(Step::execute);
    }
}
```

`StepJob` 은 `Tasklet` 을 가지고 있는 `Step` 을 `List` 로 가지고 있고 순차적으로 (`forEach`) 실행하는 Job 이다.

참고로 기존 `TaskletJob` 은 단일 `Tasklet` 을 실행 시키는 `Job` 이다. 추상클래스로 설계하여 `TaskletJob` 과 `StepJob` 의 다른 부분만 재정의 하여 확장에 용이한 구조가 되었다.

```java
public class TaskletJob extends AbstractJob {

    private final Tasklet tasklet;

    @Override
    public void doExecute() {
        tasklet.execute();
    }
}
```

<figure><img src="/files/2j7hCOiL8rxUmTmEiSXj" alt="" width="472"><figcaption></figcaption></figure>

자주 변하지 않는 로직은 슈퍼클래스에 두고 변하는 로직은 다형성을 적용하여 추상 메서드로 서브 클래스에서 재정의 하도록 디자인

[template method pattern](/second-brain/second-brain/design-pattern/template-method-pattern.md)

### Step 2. `Step` 을 구현한다.

```java
public class Step {

    private final Tasklet tasklet;

    public void execute() {
        tasklet.execute();
    }
}
```

### Step 3. 여러 Step 을 구성할 수 있는 `StepJobBuilder` 를 만든다.

```java
public class StepJobBuilder {
    private final List<Step> steps;
    private JobExecutionListener jobExecutionListener;

    public StepJobBuilder() {
        this.steps = new ArrayList<>();
    }

    public StepJobBuilder start(Step step) {
        if(steps.isEmpty()) {
            steps.add(step);
        } else {
            steps.set(0, step);
        }
        return this;
    }

    public StepJobBuilder next(Step step) {
        steps.add(step);
        return this;
    }

    public StepJobBuilder listener(JobExecutionListener jobExecutionListener) {
        this.jobExecutionListener = jobExecutionListener;
        return this;
    }

    public StepJob build() {
        return new StepJob(steps, jobExecutionListener);
    }

}
```

```java
@Bean
public Job stepExampleBatchJob(
    Step step1,
    Step step2,
    Step step3
) {
return new StepJobBuilder()
        .start(step1)
        .next(step2)
        .next(step3)
        .build();
}
```

Builder 를 통해서 메서드 체인 형식으로 좀 더 가독성이 좋은 방식으로 `StepJob` 을 생성할 수 있다.

### Step 4. 각 Step 을 구성하고 Job 에 등록한다.

```java
@Configuration
public class DormantBatchConfiguration {

    @Bean
    public Job dormantBatchJob(
            Step preDormantBatchStep,
            Step dormantBatchStep,
            DormantBatchJobExecutionListener listener
    ) {
        return new StepJobBuilder()
                .start(preDormantBatchStep)
                .next(dormantBatchStep)
                .build();
    }

    // 휴면전환 예정 1주일전인 사람에게 이메일을 발송하는 step
    @Bean
    public Step preDormantBatchStep(
            AllCustomerItemReader itemReader,
            PreDormantBatchItemProcessor itemProcessor,
            PreDormantBatchItemWriter itemWriter
    ) {
        return Step.builder()
                .itemReader(itemReader)
                .itemProcessor(itemProcessor)
                .itemWriter(itemWriter)
                .build();
    }

    // 휴면 전환 대상의 상태를 변경하는 step
    @Bean
    public Step dormantBatchStep(
            AllCustomerItemReader itemReader,
            DormantBatchItemProcessor itemProcessor,
            DormantBatchItemWriter itemWriter
    ) {
        return Step.builder()
                .itemReader(itemReader)
                .itemProcessor(itemProcessor)
                .itemWriter(itemWriter)
                .build();
    }
}
```

* 추가된 비즈니스 유스케이스 인 휴면 대상 일주일 전 고객에게 알림 발송 배치를 `preDormantBatchStep` 으로 구현하여 Job 에 추가한다.
* 각 Step 의 Tasklet 구현은 생략한다. ([Reference 의 전체 소스를 참고](/second-brain/technical/study/dont-reinvent-the-wheel/spring-batch/spring-batch-2.md#reference))

## 결론

* 배치의 비즈니스 코드는 Reader - Processor - Writer 로 관심사를 분리 할 수 있다.
* 의존성이 잘 관리 되어 단위 테스트 코드 작성도 쉬워진다.
* `@Configuration` 클래스만으로 Job 의 구성과 흐름을 파악할 수 있다.

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

## Reference

전체 소스

{% embed url="<https://github.com/viviennes7/fastcampus-batch-campus>" %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://programmer-jjy.gitbook.io/second-brain/technical/study/dont-reinvent-the-wheel/spring-batch/spring-batch-2.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
