JaeWon's Devlog
article thumbnail
반응형

최근에 회사에서 스프링배치를 사용하는 배치업무를 담당하고 있습니다.

간략하게 JobParameter를 수정할 내용이 있었는데 이를 추가할 때마다 모든 배치 소스를 수정해야 하는 불편함을 알게 되었습니다.(아래 적용하려다가 괜히 일이 커지긴 했다...)

 

해당 글에서는 이 JobParameter 를 조금 더 활용하는 법과 별도의 클래스(Class)로 만들어 공통으로 관리하고 빈(Bean)으로 등록해서 사용하는 것을 포스팅해보려고 합니다.

 

해당 글에서 사용된 소스는 Git 에서 확인하실 수 있습니다.


1. 기존 방식

- 기존에서는 JobParameter 를 사용하기 위해 아래와 같은 형태로 작성하였습니다.

1-1. 샘플코드

@Slf4j
@Configuration
@RequiredArgsConstructor
public class JobParameterTestBatchJob {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final EntityManagerFactory entityManagerFactory;

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

    @Bean(name = "jobParameterTestStep")
    @JobScope
    public Step simpleStep1() {

        return stepBuilderFactory.get("simpleStep1")
                .<Member, Member>chunk(5)
                .reader(reader(null, null)) //  (1)
                .processor(processor(null))
                .writer(writer())
                .build();
    }

    @Bean
    @StepScope
    public JpaPagingItemReader<Member> reader(
            @Value("#{jobParameters[param]}") String param,
            @Value("#{jobParameters[date]}") String date) { //  (2)

        log.info("ItemReader 에서는 데이터를 불러오는 로직 작성");
        log.info("Param : " + param);
        log.info("Date: " + date);

        Map<String, Object> parameters = new HashMap<>();
        //parameters.put("date", date);   //  (3-1) Error 발생 Type Error

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        parameters.put("date", LocalDate.parse(date, formatter)); // (3-2) 정상 조회

        return new JpaPagingItemReaderBuilder<Member>()
                .name("JobParameterTestReader")
                .entityManagerFactory(entityManagerFactory)
                .pageSize(5)
                .queryString("SELECT m FROM Member m WHERE m.createDate = :date")
                .parameterValues(parameters)
                .build();
    }

    @Bean
    @StepScope
    public ItemProcessor<Member, Member> processor(@Value("#{jobParameters[param]}") String param) {
        log.info("ItemProcessor 에서는 데이터를 처리 하는 로직 작성");
        return item -> item;
    }

    @Bean
    public ItemWriter<Member> writer() {
        log.info("ItemWriter 에서는 DB 저장과 같은 Transactional 한 로직 작성");

        return items -> {
            for(Member member: items) {
                log.info("Member Info : " + member.toString());
            }
        };
    }
}

1-2. JobParameter 사용 방식

- Batch 가 수행될 때 외부에서 JobParameter 를 세팅하여 해당 값을 꺼내어 사용합니다.

  1. reader(null, null)
    - Reader 에서 JobParameter를 사용하기 위해 Step에서 null을 임시값으로 강제 세팅.
  2. @Value("#{jobParameters[date]}") String date
    - 실제 reader 메서드에서는 null 이 전달되어도 @Value("#{jobParameters[date]}") String date로 인해 JobParamter 의 값으로 교체되어 사용.
  3. parameters.put("date", LocalDate.parse(date, formatter));
    - 전달받은 JobParameter 값을 통해 로직 수행.

- JobParameter 는 String, Boolean, Integer, Date 만 타입으로 사용할 수 있습니다.

- 이로 인해 만약 Spring Batch에서 LocalDate를 사용하기 위해서는 위에서와 같이 형변환 작업이 필요합니다.

- 이런 방식에는 아래와 같은 단점들이 발생할 수 있습니다.

  • 여러 배치가 있다면 LocalDate 로 바꾸는 코드를 매번 작성하는 중복이 발생한다.
  • processor, writer 에서는 변환된 LocalDate 값을 사용할 수 없다.(다시 형변환 작업 필요)
  • 확장성이 떨어집니다.(각 배치에서 원하는 로직이 필요하다)
  • reader(null, null), processor(null) 를 살펴보면 null을 인자로 넘기는데, 처음 보는 사람은 NPE 발생을 우려할 수 있다.

2. JobParamter 활용

- 위에 나온 단점들에 대한 대안으로 JobParamter 를 따로 가지는 클래스(Class)를 만들고, 해당 클래스를 빈(Bean)으로 등록하여 사용하는 것입니다.

2-1. 샘플코드

- JobParameter를 담을 클래스에서 실제 JobParameter 값을 주입받는 방법에는 Setter / Constructor / Field 3 가지 방법이 있습니다.

- 자세한 내용은 이곳을 통해 참고해주시면 됩니다.

- 해당 글에서는 Setter 방식을 사용하였습니다.

@Getter
public class CustomJobParameter {

    @Value("#{jobParameters[param]}")	// (1)
    private String param;
    private LocalDate date;

    @Value("#{jobParameters[date]}")	// (2)
    public void setDate(String date) {
        if(!ObjectUtils.isEmpty(date)) {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            this.date = LocalDate.parse(date, formatter);
        }
    }
}
  1. @Value("#{jobParameters[param]}")
    - 해당 파라미터는 따로 로직이 필요 없기 때문에 setter를 사용하지 않고 주입이 가능합니다.
    - Enum, Long, String의 타입은 직접 필드로 받아도 형변환이 가능합니다.
  2. @Value("#{jobParameters[date]}")
    - LocalDate와 같이 자동 형변환이 안 되는 경우에는 Setter 메서드에 @Value를 선언하여 문자열(String)로 받아 후처리(형변환)를 합니다.

- Null 체크를 무조건 진행합니다.

- 빈(Bean)으로 등록되기 때문에 CustomJobParameter에서 date(LocalDate 형변환)가 필요 없는 Job을 실행할 때는 해당 값을 전달하지 않을 것이고 그로 인해 NPE 가 발생할 수 있습니다.

- 이렇게 만든 클래스를 사용하게 되면 Batch 수행 클래스에서는 Bean을 등록하여 사용하게 됩니다.

@Slf4j
@Configuration
@RequiredArgsConstructor
public class JobParameterTestBatchJob {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final EntityManagerFactory entityManagerFactory;

    // (1-1) customJobParameter 의존성 주입
    private final CustomJobParameter customJobParameter;

    @Bean("JobParameterTestJobParameter")
    @JobScope (1-2)
    public CustomJobParameter jobParameter() {
        return new CustomJobParameter();
    }


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

    @Bean(name = "jobParameterTestStep")
    @JobScope
    public Step simpleStep1() {

        return stepBuilderFactory.get("simpleStep1")
                .<Member, Member>chunk(5)
                .reader(reader())		// (2) null 로 임의의 값 세팅하는 부분 생략가능
                .processor(processor())
                .writer(writer())
                .build();
    }

    @Bean
    @StepScope
    public JpaPagingItemReader<Member> reader() {

        log.info("ItemReader 에서는 데이터를 불러오는 로직 작성");
        log.info("Param : " + customJobParameter.getParam());
        log.info("Date: " + customJobParameter.getDate());

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("date", customJobParameter.getDate());	// (3) customJobParameter 에서 바로 꺼내어 사용 가능

        return new JpaPagingItemReaderBuilder<Member>()
                .name("JobParameterTestReader")
                .entityManagerFactory(entityManagerFactory)
                .pageSize(5)
                .queryString("SELECT m FROM Member m WHERE m.createDate = :date")
                .parameterValues(parameters)
                .build();
    }

    ...
    // processor(), writer() 는 동일
}
  1. 의존성 주입
    1-1) private final CustomJobParameter customJobParameter;
    - JobParameter를 관리하는 CustomJobParameter 클래스를 Bean 주입을 받습니다.
    - 내부 필드로 선언하였기 때문에 해당 클래스에서는 어느 곳이든 편하게 사용할 수 있습니다.
    1-2) @JobScope public CustomJobParameter jobParameter()
    - JobParamter의 @Value를 받기 위해서는 @JobScope, @StepScope 가 꼭 필요합니다.
    - @Component에도 Scope 어노테이션을 사용할 수 있습니다.
    - @Bean으로 선언할 경우 Bean 생성에 대한 여러 가지 옵션을 사용할 수 있는 장점이 있습니다.
  2. reader()
    - CustomJobParameter를 통해 내부 필드로 선언이 되어 임의로 Null 세팅을 하지 않아도 됩니다.
  3. customJobParameter.getDate()
    - 해당 클래스 내부에 있는 customJobParamter 변수를 Reader 메서드뿐만 아니라 모든 곳에서 바로 사용 가능합니다.
    - 형변환은 CustomJobParameter에서 생성 시점에 형변환이 되었기 때문에 바로 사용할 수 있습니다.

2-2. 여러 Job에서 CustomJobParamter 사용

- 위와 같은 방식으로는 1개의 Job 클래스에서만 사용이 가능합니다.

- 다른 Job 클래스에서 사용하고자 한다면 아래와 같은 오류가 발생합니다.


2개 이상의 배치 Job에서 CustomJobParameter를 위와 같은 방식으로 선언하여 사용하는 경우 발생

 

Parameter 0 of constructor in com.study.springbatch.controller.TestController required a single bean, but 2 were found:
	- simpleJob: defined by method 'simpleJob' in class path resource [com/study/springbatch/job/JobParameterTestBatchJob.class]
	- customJob: defined by method 'customJob' in class path resource [com/study/springbatch/job/MultiCustomJobParameterBatchJob.class]

- 2개의 Job(SimpleJob, CustomJob)에서 CustomJobParameter 의존 관계를 필요로 합니다.

- 이때, 두 곳에서 Bean 이 띄어지면서 타입이 동일한 Bean 이 여러 개가 스프링 컨테이너에 등록됩니다.

- 스프링에서는 어떠한 Bean을 주입받을지 알 수가 없어 오류가 발생하게 됩니다.


타입은 동일하더라도 Bean 이름이 다르기 때문에 등록 자체로는 문제가 없습니다.

- 제일 간단하게는 @Qualifier 를 선언해서 각 job에 해당 Bean 이름을 명시해서 주입받으면 됩니다.

- 하지만 위와 같은 방식은 job 이 n 개이면 n 개만큼의 Bean 이 등록되게 됩니다.

3. CustomJobParameter 공통 관리하기

- CustomJobParameter를 관리하는 Config 클래스를 생성하여 Bean을 등록하고 관리를 합니다.

@Configuration(proxyBeanMethods = false)
public class CustomJobParameterConfig {

    @Bean
    @JobScope
    public CustomJobParameter createJobParameter() {
        return new CustomJobParameter();
    }
}

- proxyBeanMethods = false는 @Bean 메서드를 호출할 때 의도적으로 매번 다른 객체가 생성되도록 합니다.

- 위 설정을 줌으로써 여러 배치에서 CustomJobParameter를 사용할 수 있게 됩니다.

- 해당 클래스를 추가함으로써 각 배치 클래스에서 CustomJobParameter 생성하는 로직을 제거합니다.

    private final CustomJobParameter customJobParameter;

    // CustomJobParameterConfig 적용 후
//    @Bean("JobParameter")
//    @JobScope
//    public CustomJobParameter createJobParameter() {
//        return new CustomJobParameter();
//    }

- 이후 2개의 배치를 수행하여 확인합니다.


참고

- https://jojoldu.tistory.com/490

- https://woodcock.tistory.com/45

- https://hodolman.tistory.com/17

반응형
profile

JaeWon's Devlog

@Wonol

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!