Hae-Riri / today-alcohol

오늘한주 - 술 레시피 공유 커뮤니티 모바일 앱
0 stars 0 forks source link

Spring Batch 만들기 #17

Open Hae-Riri opened 3 years ago

Hae-Riri commented 3 years ago

Batch 목록

Job 생성하기

Step 안에 있다고 한 Tasklet 하나랑 Reader&Processor&Writer 묶음 하나가 같은 레벨이다. 그래서 Reader & Processor 가 끝나고 Tasklet 으로 마무리 짓는 식으로는 만들 수 없다.

Tasklet은 @Bean, @Component와 비슷하게, 명확한 역할은 없으나 개발자의 커스텀 기능을 위한 단위이다.

DBMS에서 실행해보기 with 메타데이터

Spring Batch의 메타 데이터가 가진 내용은,

메타 데이터 살펴보기

1. BATCH_JOB_INSTANCE

위 코드를 수행하면 simpleJob이 실행되면서 this is step1이 출력되는데, 그 Job이 DB에 기록되어 있다. 얘는 Job 파라미터에 따라 생성 되는 테이블인데 Job Parameter는 외부에서 받을 수 있는 파라미터다. 예를 들어 특정 날짜를 넘겨서 Job 안에서 작업할 수도 있고 출력할 수도 있다. 같은 Batch Job이더라도 Job Parameter가 다르면 이 테이블에 기록되고, Job Parameter가 다르면 기록되지 않는다. 위에서 실행한 simple job 코드를 변경해보자.

@Slf4j // log 사용을 위한 lombok 어노테이션
@RequiredArgsConstructor // 생성자 DI를 위한 lombok 어노테이션
@Configuration
public class SimpleJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job simpleJob() {
        return jobBuilderFactory.get("simpleJob")
                .start(simpleStep1(null))
                .build();
    }

    @Bean
    @JobScope //(1)
    public Step simpleStep1(@Value("#{jobParameters[requestDate]}") String requestDate) { //(2)
        return stepBuilderFactory.get("simpleStep1")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> This is Step1");
                    log.info(">>>>> requestDate = {}", requestDate); //(3)
                    return RepeatStatus.FINISHED;
                })
                .build();
    }
}

(1) @JobScope를 빼먹으면 안된다. 이건 Step 선언문에서 사용 가능하고, Bean Scope를 지원한다. 스프링 빈의 기본 scope는 싱글톤인데, Spring Batch 컴포넌트인 Tasklet, ItemReader, ItemWriter, ItemProcessor 등에 @Bean 밑에 @JobScope를 해주면 저장된 Job의 실행시점에 해당 컴포넌트를 스프링 빈으로 등록한다. @StepScope도 Step 시작 시점에 스코프가 실행된다. 즉, Bean의 생성 시점을 지연시키는 것이다.

bean의 생성시점을 지연하는 것으로 인해 얻는 장점으로는 아래 두 가지가 있다.

  1. Job Parameter를 Late Binding할 수 있음.

    Job Parameter를 StepContext나 JobExecutionContext 레벨에서 할당시킬 수 있다. 꼭 어플리케이션이 실행되는 시점이 아니더라도 Controller나 Service와 같은 비즈니스 로직을 처리하는 부분에서 Job Parameter를 할당시킬 수 있는 것이다.

  2. 동일한 컴포넌트를 병렬 혹은 동시에 사용할 때 유용하다.

    Step 안에 Tasklet이 있고 이 Tasklet은 멤버 변수를 갖고 있으면서 이 멤버변수를 변경하는 로직을 갖고 있을 때 @StepScope없이 Step을 병렬로 실행시킨다고 하자. 그러면 서로 다른 Step에서 하나의 Tasklet을 두고 마구잡이로 상태를 변경하려고 하게 될 것이다. 싱글톤으로 관리되는 step을 여러번 실행시키니까. / 그러나 stepScope을 사용한다면 각각의 Step에서 별도의 Tasklet을 생성하고 관리하기 때문에 서로의 상태를 침범하지 않는다.

(2) jobParameter를 설정하는 부분이다. edit configuration의 Program arguments를 통해 전달해서 실행하면 (3)의 내용이 로그에 찍히고, 기존과 다른 Job Parameter를 가졌기 때문에 테이블에 새로 기록된다.

이 상태에서 같은 파라미터로 다시 한번 Batch를 그대로 실행해보면 JobInstanceAlreadyCompleteException과 함께 job parameter를 변경하라고 한다. requestDate의 값을 변경하면 테이블에 잘 저장되고 배치도 실행된다. 즉, 동일한 Job Parameter는 여러 개 존재할 수 없다. 정확히 말하면 동일한 Job Parameter로 '성공'한 기록이 있을 때만 재수행이 안된다. 실패 기록만 있다면 같은 job parameter로 해도 실행된다!

2. BATCH_JOB_EXECUTION

3. BATCH_JOB_EXECUTION_PARAMS

BATCH_JOB_FLOW

위에서 작성한 로그에 찍는 것과 같은 실제 작업은 Job이 아니라 Job안에 있는 Step에서 수행한다. 이처럼 Batch 처리할 내용을 담고 있는 게 Step이기 때문에 Job 내부에서 Step들 간 순서 혹은 처리 흐름을 제어할 필요가 있다. 여러 Step들을 어떻게 관리할까.

Next

@Slf4j
@Configuration
@RequiredArgsConstructor
public class StepNextJobConfiguration {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job stepNextJob() {
        return jobBuilderFactory.get("stepNextJob")
                .start(step1())
                .next(step2())
                .next(step3())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get("step1")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> This is Step1");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }

    @Bean
    public Step step2() {
        return stepBuilderFactory.get("step2")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> This is Step2");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }

    @Bean
    public Step step3() {
        return stepBuilderFactory.get("step3")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> This is Step3");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }
}

조건별로 흐름 제어 (Flow)

Decide

위에서 진행한 Flow(분기), next(다음스텝순차적 정함)에는 문제가 있다.

  1. Step이 담당하는 역할이 2개 이상이 된다. : 실제 Step이 처리해야 할 로직 외에도 분기 처리를 하기 위한 Exit Status 조정 필요
  2. 다양한 분기 로직 처리 어려움 : Exit Status를 커스텀하고 고치기 위해 Listener를 새로 생성하고 Job Flow에 등록해야 한다는 수고로움이 있음. 따라서, 명확하게 Step들 간의 Flow 분기만 담당하면서, 다양한 분기처리가 가능한 타입이 필요해서 생긴 게 Decide다.

아래 예제는 start Step -> oddDecider에서 홀짝 판별 -> oddStep 또는 evenStep 진행 이걸 구현한 decider를 flow 안에 넣는 방법은 아래와 같다.

@Bean
    public Job deciderJob() {
        return jobBuilderFactory.get("deciderJob")
                .start(startStep())
                .next(decider()) // startStep 이후에 decider()를 실행
                .from(decider()) // decider의 상태가
                    .on("ODD") // ODD라면
                    .to(oddStep()) // oddStep로 간다.
                .from(decider()) // decider의 상태가
                    .on("EVEN") // ODD라면
                    .to(evenStep()) // evenStep로 간다.
                .end() // builder 종료
                .build();
    }
//decider() 구현체
    @Bean
    public JobExecutionDecider decider() {
        return new OddDecider();
    }

    public static class OddDecider implements JobExecutionDecider {

        @Override
        public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
            Random rand = new Random();

            int randomNumber = rand.nextInt(50) + 1;
            log.info("랜덤숫자: {}", randomNumber);

            if(randomNumber % 2 == 0) {
                return new FlowExecutionStatus("EVEN");
            } else {
                return new FlowExecutionStatus("ODD");
            }
        }
    }

Batch Scope와 Job Parameter

위에서 @JobScope, @StepScope에 대해 언급한 적이 있다. Job Parameter 값을 변경하려 할 때 준 것 같은데, 어쨌든 Spring Batch에서는 외부 혹은 내부 파라미터를 받아 여러 Batch 컴포넌트에서 사용할 수 있게 지원한다. 이 파라미터를 Job Paramter라고 하는데, 이걸 사용하기 위해서는 항상 @JobScope, @StepScope 등 전용 Scope를 선언해야 한다.

@StepScope, @JobScope 소개

Spring Batch 컴포넌트(Tasklet, ItemReader, ItemWriter, Processor 등)에 @StepScope를 사용하면 Spring Batch가 컨테이너를 통해 지정된 Step의 실행 시점에 해당 컴포넌트를 bean으로 등록한다. 그래서 bean의 생성 시점이 애플리케이션 시작 시점이 아닌 Scope가 시작되는 시점이 된다. 이렇게 실행 시점을 달리 했을 때의 좋은 점은 위에서 말했으니 넘어간다.

Job Parameter에 대해

Job Paramter는 @Value를 통해서 가능하고, Scope Bean(@JobScope나 @StepScope의 bean)을 생성해야만 가능하며, 생성하는 시점에 Job Parameter를 호출할 수 있다. 만약 일반적인 single tone bean으로 생성하게 되면 job Parameters cannot be found 에러가 발생한다. scope bean을 파라미터로 생성하든 클래스의 멤버변수로 생성하든 상관은 없으나 JobParamter를 사용하려면 반드시 @StepScope, @JobScope를 통해 scope bean을 생성해야 한다.

StepScope 사용 시 주의사항

reader()에서 ItemReader 타입을 리턴할 때 @StepScope 안에 proxyMode = ScopedProxyMode.TARGET_CLASS가 있기 때문에 ItemReader 인터페이스의 프록시 객체를 생성하여 리턴한다. @StepScope가 없으면 프록시가 아닌 생성한 객체 그대로를 bean으로 전달하는데, 이걸 쓰면 프록시 객체가 사용되는 것이다.

chunk 지향 처리

READER

Spring Batch가 Chunk 지향 처리를 하고 이는 Job과 Step으로 구성되어 있다. Step은 Tasklet단위로 처리되고, Tasklet 중에서 ChunkOrientedTasklet을 통해 Chunk를 처리하며, 이걸 구성하는 요소에는 ItemReader, ItemWriter, ItemProcessor가 있다. ItemReader & ItemWriter & ItemProcessor의 묶음 도 Tasklet이다. ChunkOrientedTasklet에서 관리하니까

Reader로 읽을 수 있는 데이터

WRITER

Processor는 선택이다. Processor 없이도 chunkOrientedTasklet을 구성할 수 있다. 하지만 Reader, Writer는 필수다.

PROCESSOR

ItemProcessor는 데이터를 가공하거나 필터링하는 역할을 한다. 이건 필수가 아니고, writer에서도 충분히 구현이 가능하다. 그런데도 processor를 사용하는 이유는, 비즈니스 코드가 섞이는 것을 방지하기 위해서이다.

ItemProcessor를 사용하는 방법

Hae-Riri commented 3 years ago

core/dev,local,real,stage/ url.properties 에 도메인 별 세팅 필요

WebtoonMemberCancelClient

CancelUserReader

CancelUserWriter

McmsCancelClient