Skip to content

Latest commit

ย 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
ย 
ย 
ย 
ย 
ย 
ย 

README.md

8. Chunk ์ง€ํ–ฅ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์˜ ๊ธฐ๋ณธ๊ตฌ์„ฑ๊ณผ ์•„ํ‚คํ…์ฒ˜

๋ฐฐ์น˜ ์ž‘์—…์€ ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค๋Š” ํŠน์ง•์„ ๊ฐ€์ง„๋‹ค. Spring Batch์—์„œ๋Š” ์ด๋ฅผ "์ฒญํฌ"๋ผ๋Š” ๋‹จ์œ„๋กœ ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๋ฅผ ๋‚˜๋ˆ„์–ด ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์œผ๋กœ ์ฒญํฌ ๊ธฐ๋ฐ˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค.

ํ•œ ๋ฒˆ์— ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋Š”๊ฒŒ ์•„๋‹Œ, ์ง€์ •๋œ ์ฒญํฌ ํฌ๊ธฐ๋งŒํผ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ปค๋ฐ‹/๋กค๋ฐฑํ•  ์ˆ˜ ์žˆ๋‹ค. ๋˜ํ•œ, ๋ฐ์ดํ„ฐ๋ฅผ ๋‚˜๋ˆ„์–ด ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•˜์—ฌ ์„ฑ๋Šฅ์„ ๊ทน๋Œ€ํ™” ์‹œํ‚ฌ ์ˆ˜๋„ ์žˆ๋‹ค.

8.1 ๋‹จ์ˆœ Tasklet ๊ตฌํ˜„๊ณผ ChunkOrientedTasklet์˜ ์ฐจ์ด

Chunk๋Š” ํ•œ ํŠธ๋žœ์žญ์…˜์—์„œ ์ฒ˜๋ฆฌ๋  ๋ฐ์ดํ„ฐ ๋ฉ์–ด๋ฆฌ๋ฅผ ์˜๋ฏธํ•œ๋‹ค. ๊ตฌ์ฒด์ ์œผ๋กœ ์•Œ์•„๋ณด๊ธฐ ์œ„ํ•ด, ์•„๋ž˜ ์ฒญํฌ ์ง€ํ–ฅ ์ฒ˜๋ฆฌ ๋‹ค์ด์–ด๊ทธ๋žจ์„ ์‚ดํŽด๋ณด์ž.

img.png

์ •์˜ํ•œ ์ž‘์—…์ด ์‹คํ–‰๋˜๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ Chunk ์ˆ˜๋งŒํผ ์ฝ์–ด์˜จ๋‹ค. ์ด๋•Œ, ํ•˜๋‚˜์”ฉ ์ฝ์–ด์˜ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ์ฆ‰, ๋ฐ์ดํ„ฐ๋ฅผ Chunk ์ˆ˜๋งŒํผ ๋ฐ˜๋ณตํ•ด์„œ ํ•˜๋‚˜์”ฉ ์ฝ์–ด์˜ค๋Š” ๊ฒƒ์ด๋‹ค.
๊ทธ๋ฆฌ๊ณ , ํ•˜๋‚˜์˜ Chunk(์ฝ์–ด๋“ค์ธ ๋ฐ์ดํ„ฐ ๋ฉ์–ด๋ฆฌ)๋ฅผ ํ•œ๋ฒˆ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•œ๋‹ค.

์ด๋ฅผ ์Šˆ๋„์ฝ”๋“œ๋กœ ๋‚˜ํƒ€๋‚ด๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

List items = new Arraylist();
for(int i = 0; i < commitInterval; i++){
    Object item = itemReader.read();
    if (item != null) {
        items.add(item);
    }
}
itemWriter.write(items);

๋‹จ์ˆœํžˆ Tasklet::execute()๋ฅผ ๊ตฌํ˜„ํ•œ ๋ฐฉ์‹๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ Chunk๋ผ๋Š” ๊ฐœ๋…์ด ์‚ฌ์šฉ๋œ ๊ฒƒ์ด๋‹ค.
Tasklet::execute()๋ฅผ ํ†ตํ•ด ๋‹จ์ผ ํƒœ์Šคํฌ๋ฅผ ์ •์˜ํ•˜๋Š” ๊ฒƒ์€ RepeatStatus์„ ์‚ฌ์šฉํ•ด์„œ Tasklet์„ ๋ฐ˜๋ณต์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ด์™”๋‹ค.

๋ฐ˜๋ฉด์—, ์ฒญํฌ ๊ธฐ๋ฐ˜ ์ž‘์—…์€ Tasklet ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋‹จ์ˆœํžˆ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ChunkOrientedTasklet๋ผ๋Š” ๊ฐ์ฒด๋กœ ์ฒญํฌ ๊ธฐ๋ฐ˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. ChunkOrientedTasklet๋Š” ์–ด๋–ป๊ฒŒ ๋‹ค๋ฅธ์ง€ ์•„๋ž˜ ๊ทธ๋ฆผ์œผ๋กœ ์‚ดํŽด๋ณด์ž.

  • Tasklet::execute() ์ง์ ‘ ๊ตฌํ˜„



  • ChunkOrientedTasklet

์–ด์จŒ๋“  ๋‘˜ ๋‹ค Tasklet::execute()๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฒƒ์ด๋ฉฐ, ์ฐจ์ด์ ์€ ๊ตฌํ˜„๋ฐฉ์‹์˜ ์ฐจ์ด๋‹ค. Tasklet::execute()๋ฅผ ์ง์ ‘ ์ปค์Šคํ…€ํ•˜๊ฒŒ ๊ตฌํ˜„ํ•œ๋‹ค๋ฉด ๋‹จ์ผ ํƒœ์Šคํฌ๋กœ ๊ตฌํ˜„๋œ๋‹ค.
๋ฐ˜๋ฉด์—, Chunk ์ง€ํ–ฅ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ChunkOrientedTasklet์€ Reader & Processor & Writer ๋ฌถ์Œ์œผ๋กœ ํ•˜๋‚˜์˜ Tasklet์„ ์ •์˜ํ•œ๋‹ค.
์ปค์Šคํ…€ํ•œ Tasklet ๊ตฌํ˜„์€ Chunk ๋‹จ์œ„์˜ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š” ์—†๊ฑฐ๋‚˜ ํ•˜๋Š” ๊ฒฝ์šฐ์— ๋ณดํ†ต ์‚ฌ์šฉ๋œ๋‹ค.

์ด์ œ๋ถ€ํ„ฐ ChunkOrientedTasklet๊ฐ€ ์–ด๋–ป๊ฒŒ ํƒœ์Šคํฌ ์ˆ˜ํ–‰์„ ํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž.


8.2 Chunk ์ง€ํ–ฅ ์ฒ˜๋ฆฌ์˜ ๊ธฐ๋ณธ ๊ตฌ์„ฑ

Chunk ์ง€ํ–ฅ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด, Job์„ ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑํ•˜๋Š”์ง€ ์˜ˆ์‹œ๋ฅผ ํ†ตํ•ด ์•Œ์•„๋ณด์ž.

public class TransferNewUserJobConfiguration {

    // 1. Job ์ •์˜
    @Bean
    public Job transferNewUserJob() {
        return new JobBuilder("TRANSFER_NEW_USER_JOB", jobRepository)
            .start(transferNewUserStep(null))
            .build();
    }

    // 2. Step ์ •์˜ (Chunk ์ง€ํ–ฅ Step ๊ตฌ์„ฑ: Reader/Processor/Writer)
    @Bean
    @JobScope
    public Step transferNewUserStep(
        @Value("#{jobParameters['targetDate']}") LocalDate targetDate
    ) {
        return new StepBuilder("TRANSFER_NEW_USER_STEP", jobRepository)
            .<User, User>chunk(CHUNK_SIZE, platformTransactionManager)
            .reader(reader())
            .processor(processor(null))
            .writer(writer())
            .build();
    }

    // 3. Reader ์ •์˜
    @Bean
    @StepScope
    public JpaPagingItemReader<User> reader() {
        return new JpaPagingItemReaderBuilder<User>()
            .name("TRANSFER_NEW_USER_STEP_READER")
            .entityManagerFactory(entityManagerFactory)
            .queryString("""
                SELECT u
                FROM User u
                """)
            .pageSize(CHUNK_SIZE)
            .build();
    }

    // 4. Processor ์ •์˜
    @Bean
    @StepScope
    public ItemProcessor<User, User> processor(
        @Value("#{jobParameters['targetDate']}") final LocalDate targetDate
    ) {
        return new FunctionItemProcessor<>(user -> {
            if (user.getRegisteredAt().toLocalDate().isEqual(targetDate)) {
                return user;
            }
            return null;
        });
    }

    // 5. Writer ์ •์˜
    @Bean
    @StepScope
    public ItemWriter<User> writer() {
        return chunk -> chunk.getItems().forEach(user ->
            log.info("DB์— ์œ ์ € ์ •๋ณด ์ €์žฅ =>  id: {}, name: {}", user.getId(), user.getName())
        );
    }
}
  1. Job์„ ์ •์˜ํ•œ๋‹ค. Job์€ Step์œผ๋กœ ๊ตฌ์„ฑ๋œ๋‹ค๋Š” ๊ฒƒ์€ ๋ณ€ํ•จ์—†๋‹ค.
  2. Step์„ ์ •์˜ํ•œ๋‹ค. Step์„ ์ •์˜ํ•  ๋•Œ, StepBuilder์˜ Chunk ์ง€ํ–ฅ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด chunk()๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ , Chunk ์ง€ํ–ฅ ์ฒ˜๋ฆฌ๋‹ต๊ฒŒ Reader, Processor, Writer๋กœ Step์˜ ๋‹จ๊ณ„๋ฅผ ๊ตฌ๋ถ„์ง€์–ด ์ •์˜ํ–ˆ๋‹ค.
    StepBuilder::chunk() ๋ฉ”์„œ๋“œ๋Š” ๋‹ค์Œ ์ ˆ์—์„œ ์‚ดํŽด๋ณด์ž.
  3. Reader๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š” ์—ญํ• ์„ ํ•˜๊ณ , ์—ฌ๊ธฐ์„œ๋Š” ItemReader์˜ ๊ตฌํ˜„์ฒด์ธ JpaPagingItemReader๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค. ์ž…๋ ฅ ํƒ€์ž…์„ User๋กœ ์ •์˜ํ–ˆ๋‹ค. ์ด ์ž…๋ ฅ ํƒ€์ž…์€ Processor๋กœ ๋„˜์–ด๊ฐ€๊ฒŒ ๋œ๋‹ค.
  4. Processor๋Š” ์ž…๋ ฅ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์„œ, ์ถœ๋ ฅ ๋ฐ์ดํ„ฐ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” User ํƒ€์ž…์„ ๋ฐ›์•„, User๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ์˜๋ฏธ๋กœ ItemProcessor<User, User>๋ผ๊ณ  ๋ฐ˜ํ™˜ํƒ€์ž…์„ ๋ช…์‹œํ–ˆ๋‹ค.
  5. Writer๋Š” Processor๋กœ๋ถ€ํ„ฐ ๊ฐ€๊ณต๋œ Userํƒ€์ž…์˜ ๋ฐ์ดํ„ฐ๋“ค์„ chunk ๋‹จ์œ„๋กœ ๋ฐ›์•„์„œ, ํ•œ๋ฒˆ์— DB์— ์“ฐ๊ธฐ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

ItemReader, ItemProcessor, ItemWriter์˜ ๋™์ž‘ ๋ฐฉ์‹์— ๋Œ€ํ•ด์„œ๋Š” ๋ณ„๋„๋กœ ๊ธ€๋กœ ์•Œ์•„๋ณด๊ณ , ์—ฌ๊ธฐ์„œ๋Š” StepBuilder์˜ chunk()๋ฉ”์„œ๋“œ์˜ ๋™์ž‘ ๋ฐฉ์‹์— ๋Œ€ํ•ด์„œ๋งŒ ์•Œ์•„๋ณด์ž.


8.3 ChunkOrientedTasklet์˜ ์ƒ์„ฑ ๋ฐฉ์‹(w. StepBuilder::chunk())

8.2์—์„œ ๋ณด์•˜๋“ฏ์ด ์ฒญํฌ ์ง€ํ–ฅ ๋ฐฉ์‹์˜ Step ์ƒ์„ฑ์„ ์œ„ํ•ด์„œ StepBuilder์˜ chunk() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ–ˆ๋‹ค.
์ด ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ์„ ํ†ตํ•ด ChunkOrientedTasklet์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ํ•œ๋ฒˆ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ดํŽด๋ณด์ž.

public class StepBuilder extends StepBuilderHelper<StepBuilder> {
    
    // SimpleStepBuilder๋ฅผ ๋ฐ˜ํ™˜
    public <I, O> SimpleStepBuilder<I, O> chunk(int chunkSize, PlatformTransactionManager transactionManager) {
        return ((SimpleStepBuilder)(new SimpleStepBuilder(this)).transactionManager(transactionManager)).chunk(chunkSize);
    }
}

chunk() ๋ฉ”์„œ๋“œ๋Š” SimpleStepBuilder๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์ด๋Š” TaskletStep์„ ์ƒ์„ฑํ•ด์ฃผ๋Š” bulid()๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. ์•ž์„œ, Tasklet::execute()๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋‹จ์ผ ํƒœ์Šคํฌ ๋ฐฉ์‹์—์„œ๋„ Tasklet์„ ์ƒ์„ฑํ•˜๊ธด ํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋•Œ๋Š” StepBuilder::tasklet() ํ˜ธ์ถœ์„ ํ†ตํ•ด TaskletStepBuilder๋ฅผ ๋ฐ˜ํ™˜๋ฐ›์€ ๋ฐ˜๋ฉด, StepBuilder::chunk()์„ ํ†ตํ•ด SimpleStepBuilder์„ ๋ฐ˜ํ™˜๋ฐ›๋Š”๋‹ค.

SimpleStepBuilder์˜ ๋‚ด๋ถ€ ๊ตฌ์กฐ๋ฅผ ๋ณด๋ฉด์„œ ์–ด๋–ป๊ฒŒ ChunkOrientedTasklet์ด ์ƒ์„ฑ๋˜๋Š”์ง€ ํ™•์ธํ•ด๋ณด์ž.

public class SimpleStepBuilder<I, O> extends AbstractTaskletStepBuilder<SimpleStepBuilder<I, O>> {
    private ItemReader<? extends I> reader;
    private ItemWriter<? super O> writer;
    private ItemProcessor<? super I, ? extends O> processor;

    public TaskletStep build() {
        this.registerStepListenerAsItemListener();
        this.registerAsStreamsAndListeners(this.reader, this.processor, this.writer);
        return super.build();
    }

    protected Tasklet createTasklet() {
        Assert.state(this.reader != null, "ItemReader must be provided");
        Assert.state(this.writer != null, "ItemWriter must be provided");
        RepeatOperations repeatOperations = this.createChunkOperations();
        SimpleChunkProvider<I> chunkProvider = new SimpleChunkProvider(this.getReader(), repeatOperations);
        SimpleChunkProcessor<I, O> chunkProcessor = new SimpleChunkProcessor(this.getProcessor(), this.getWriter());
        chunkProvider.setListeners(new ArrayList(this.itemListeners));
        chunkProvider.setMeterRegistry(this.meterRegistry);
        chunkProcessor.setListeners(new ArrayList(this.itemListeners));
        chunkProcessor.setMeterRegistry(this.meterRegistry);
        ChunkOrientedTasklet<I> tasklet = new ChunkOrientedTasklet(chunkProvider, chunkProcessor);
        tasklet.setBuffering(!this.readerTransactionalQueue);
        return tasklet;
    }

    public SimpleStepBuilder<I, O> reader(ItemReader<? extends I> reader) {
        this.reader = reader;
        return this;
    }

    public SimpleStepBuilder<I, O> writer(ItemWriter<? super O> writer) {
        this.writer = writer;
        return this;
    }

    public SimpleStepBuilder<I, O> processor(ItemProcessor<? super I, ? extends O> processor) {
        this.processor = processor;
        return this;
    }
    
}

ํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ๋“ค๋งŒ ๊ฐ€์ ธ์™”๋‹ค.
8.2์ ˆ์—์„œ Step์„ ๊ตฌ์„ฑํ•  ๋•Œ, reader(), processor(), writer() ์ˆœ์„œ๋กœ ๊ตฌ์„ฑํ–ˆ์—ˆ๋‹ค. ๊ทธ๋•Œ ํ˜ธ์ถœ๋˜๋Š” ๋ฉ”์„œ๋“œ๊ฐ€ ์œ„ ๋ฉ”์„œ๋“œ๋“ค์ด๋‹ค.
๊ทธ๋ฆฌ๊ณ  build()๋ฉ”์„œ๋“œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด, ๋ถ€๋ชจ์˜ build()๋ฅผ ํ˜ธ์ถœํ•˜๋Š”๋ฐ, ์ฝ”๋“œ๋กœ ํ•œ๋ฒˆ ์‚ดํŽด๋ณด์ž.

public abstract class AbstractTaskletStepBuilder<B extends AbstractTaskletStepBuilder<B>> extends StepBuilderHelper<B> {
    // ...
    public TaskletStep build() {
        TaskletStep step = new TaskletStep(this.getName());
        
        // ...
        step.setTasklet(this.createTasklet()); // TaskletStep์ด ์‹คํ–‰ํ•  Tasklet ๋“ฑ๋ก
        return step;
    }
}

SimpleStepBuilder์˜ ๋ถ€๋ชจ์ธ AbstractTaskletStepBuilder์˜ build()๋ฉ”์„œ๋“œ๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ TaskletStep์„ ์ƒ์„ฑํ•˜๊ณ  Tasklet์„ ์ •์˜ํ•  ๋•Œ, createTasklet() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. ์ด createTasklet() ๋ฉ”์„œ๋“œ๋Š” SimpleStepBuilder์— ๊ตฌํ˜„๋˜์–ด ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋กœ, ์œ„ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด ChunkOrientedTasklet๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

๋‚ด๋ถ€์ ์œผ๋กœ ์–ด๋–ป๊ฒŒ ChunkOrientedTasklet์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ์•Œ์•„๋ดค์ง€๋งŒ, ๊ฒฐ๊ตญ์—๋Š” SimpleStepBuilder์ด TaskletStep์„ ์ƒ์„ฑํ•˜๊ณ , ๋‚ด๋ถ€ Tasklet์„ ์ฒญํฌ ์ง€ํ–ฅ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์ธ ChunkOrientedTasklet์œผ๋กœ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ด๋‹ค.


8.4 ChunkOrientedTasklet์˜ ์‹คํ–‰ ๋ฐฉ์‹

ChunkOrientedTasklet๋Š” ๊ฒฐ๊ตญ ์ฒญํฌ ์ง€ํ–ฅ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ๋กœ์ง์ด ์ •์˜๋˜์–ด ์žˆ๋Š” Tasklet์ธ ์…ˆ์ด๋‹ค. ๋•Œ๋ฌธ์—, Tasklet์„ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๊ณ , ๊ฒฐ๊ตญ์—๋Š” Tasklet::execute()๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์„ ๊ฒƒ์ด๋‹ค.(Tasklet ๊ธ€ ์ฐธ๊ณ )

๊ตฌํ˜„ ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ๋ณด๋ฉฐ ์•Œ์•„๋ณด์ž.

public class ChunkOrientedTasklet<I> implements Tasklet {
    private static final String INPUTS_KEY = "INPUTS";
    private final ChunkProcessor<I> chunkProcessor;
    private final ChunkProvider<I> chunkProvider;
    private boolean buffering = true;

    public ChunkOrientedTasklet(ChunkProvider<I> chunkProvider, ChunkProcessor<I> chunkProcessor) {
        this.chunkProvider = chunkProvider;
        this.chunkProcessor = chunkProcessor;
    }

    public void setBuffering(boolean buffering) {
        this.buffering = buffering;
    }

    @Nullable
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        Chunk<I> inputs = (Chunk)chunkContext.getAttribute("INPUTS");
        if (inputs == null) {
            inputs = this.chunkProvider.provide(contribution); // 1. Reader๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด๋“œ๋ฆฐ๋‹ค.
            if (this.buffering) {
                chunkContext.setAttribute("INPUTS", inputs);
            }
        }

        this.chunkProcessor.process(contribution, inputs); // 2. Processor์™€ Writer ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•œ๋‹ค. 
        this.chunkProvider.postProcess(contribution, inputs);
        if (inputs.isBusy()) {
            return RepeatStatus.CONTINUABLE;
        } else {
            chunkContext.removeAttribute("INPUTS");
            chunkContext.setComplete();
            return RepeatStatus.continueIf(!inputs.isEnd());
        }
    }
}

์œ„ ํด๋ž˜์Šค๋Š” ChunkOrientedTasklet ํด๋ž˜์Šค(Spring Batch 5.1 ๊ธฐ์ค€)์—์„œ ๋กœ๊น…์„ ์ œ์™ธํ•œ ์ „์ฒด ๋กœ์ง์„ ์ „๋ถ€ ๊ฐ€์ ธ์˜จ ๊ฒƒ์ด๋‹ค.
๊น”๋”ํ•˜๊ฒŒ execute() ๋ฉ”์„œ๋“œ๋งŒ ์ •์˜๋˜์–ด ์žˆ๋‹ค.

์ฃผ์„์ด ๋‹ฌ๋ฆฐ chunkProvider.provide()์™€ chunkProcessor.process()๋งŒ ์„ธ๋ถ€์ ์œผ๋กœ ์ดํ•ดํ•˜๋ฉด ์ถฉ๋ถ„ํ•  ๊ฒƒ ๊ฐ™๋‹ค. ๊ฐ๊ฐ Reader, Processor&Writer ๋กœ์ง์„ ๋‹ด๋‹นํ•œ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ๊ฐ๊ฐ SimpleChunkProvider ํด๋ž˜์Šค์™€ SimpleChunkProviderํด๋ž˜์Šค๋งŒ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋‹ค.


8.4.1 SimpleChunkProvider

public class SimpleChunkProvider<I> implements ChunkProvider<I> {

    protected final ItemReader<? extends I> itemReader;
    private final RepeatOperations repeatOperations;
    
    public Chunk<I> provide(final StepContribution contribution) throws Exception {
        Chunk<I> inputs = new Chunk(new Object[0]);
        this.repeatOperations.iterate((context) -> { // ๋ฐ˜๋ณตํ•ด์„œ ์•„์ดํ…œ์„ ์ฝ์Œ 
            Object item;
            try {
                item = this.read(contribution, inputs); // Reader::read()๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ ์ฝ์–ด๋“ค์ž„
            } catch (SkipOverflowException var12) {
                status = "FAILURE";
                var8 = RepeatStatus.FINISHED;
            } finally {
                this.stopTimer(sample, contribution.getStepExecution(), status);
            }

            if (item == null) {
                inputs.setEnd();
                return RepeatStatus.FINISHED;
            }
            inputs.add(item); // ๋ฆฌ์ŠคํŠธ์— ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
            contribution.incrementReadCount();
            return RepeatStatus.CONTINUABLE;
        });
        return inputs;
    }
}

protected I read(StepContribution contribution, Chunk<I> chunk) throws SkipOverflowException, Exception {
    return this.doRead();
}

protected final I doRead() throws Exception {
    
    // ...
    this.listener.beforeRead();
    I item = this.itemReader.read();
    if (item != null) {
        this.listener.afterRead(item);
    }
    return item;
}

provide() ๋ฉ”์„œ๋“œ๋Š” repeatOperations๋ฅผ ํ†ตํ•ด ๋ฐ˜๋ณตํ•ด์„œ ์•„์ดํ…œ์„ ์ฝ์–ด๋“ค์ธ๋‹ค. RepeatOperations์˜ ๊ตฌํ˜„์ฒด๋Š” RepeatTemplate์œผ๋กœ ์ดˆ๊ธฐํ™” ๋˜์–ด ์žˆ๋‹ค. Tasklet์„ ์„ค๋ช…ํ•˜๋Š” ๊ธ€์—์„œ RepeatTemplate์˜ completionPolicyํ•„๋“œ์— ๋Œ€ํ•ด ์„ค๋ช…ํ•œ ์ ์ด ์žˆ๋‹ค.

SimpleChunkProvider์—์„œ ์ดˆ๊ธฐํ™” ๋˜์–ด ์žˆ๋Š” RepeatTemplate์˜ CompletionPolicy๋Š” SimpleCompletionPolicy ํด๋ž˜์Šค์˜ ๊ฐ์ฒด๊ฐ€ ์ดˆ๊ธฐํ™” ๋˜์–ด ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ , SimpleCompletionPolicy๊ฐ€ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋Š” isComplete() ๋ฉ”์„œ๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

read() ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•ด์„œ๋Š” ItemReader ์ฑ•ํ„ฐ์—์„œ ์„ค๋ช…ํ•˜๊ณ  ์žˆ์œผ๋‹ˆ ์ฐธ๊ณ ํ•˜์ž.

public class SimpleCompletionPolicy extends DefaultResultCompletionPolicy {
    public boolean isComplete() {
        return this.getStartedCount() >= SimpleCompletionPolicy.this.chunkSize;
    }
}

์ฆ‰, SimpleChunkProvider ๋‚ด๋ถ€์— ์ดˆ๊ธฐํ™” ๋˜์–ด ์žˆ๋Š” RepeatTemplate์˜ CompletePolicy๋Š” SimpleCompletionPolicy์ด๋ฉฐ, ํ•ด๋‹น ํด๋ž˜์Šค๋Š” Step ์ƒ์„ฑ ์‹œ ์„ค์ •ํ•œ chunk size์˜ ๊ฐ’๊ณผ ๊ฐ™๊ฑฐ๋‚˜ ํฌ๋‹ค๋ฉด ์™„๋ฃŒ ์ฒ˜๋ฆฌ๋ฅผ ํ•œ๋‹ค. ์ดํ›„์—๋Š” ๋” ์ด์ƒ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด๋“ค์ด์ง€ ์•Š๋Š”๋‹ค.
(RepeatTemplate์ด๋‚˜ CompletionPolicy ๋“ฑ๊ณผ ๊ด€๋ จ๋œ ๊ฐœ๋…์€ batch05์—์„œ ์ •๋ฆฌํ•˜๊ณ  ์žˆ๋‹ค.)

์–ด์จŒ๋“ , ์ด๋ ‡๊ฒŒ ์ฝ์–ด๋“ค์ธ ๋ฐ์ดํ„ฐ๋ฅผ chunkProcessor.process()๋กœ ๋„˜๊ฒจ ๋‚˜๋จธ์ง€ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.


8.4.2 SimpleChunkProcessor

ChunkOrientedTasklet::execute() ๋‚ด๋ถ€์—์„œ chunkProvider.provide(contribution)์„ ํ†ตํ•ด inputs ํ•„๋“œ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด๋“ค์˜€๋‹ค.
๊ทธ๋ฆฌ๊ณ , ์ฝ์–ด๋“œ๋ฆฐ ๋ฐ์ดํ„ฐ๋ฅผ SimpleChunkProcessor ๊ตฌํ˜„์ฒด์˜ process() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋„˜๊ธด๋‹ค.

public class SimpleChunkProcessor<I, O> implements ChunkProcessor<I>, InitializingBean {

    private ItemProcessor<? super I, ? extends O> itemProcessor;
    private ItemWriter<? super O> itemWriter;

    public final void process(StepContribution contribution, Chunk<I> inputs) throws Exception {
        this.initializeUserData(inputs);
        if (!this.isComplete(inputs)) {
            Chunk<O> outputs = this.transform(contribution, inputs); // 1. Processor๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ ๊ฐ€๊ณต
            contribution.incrementFilterCount((long)this.getFilterCount(inputs, outputs));
            this.write(contribution, inputs, this.getAdjustedOutputs(inputs, outputs)); // 2. Writer๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ ์“ฐ๊ธฐ ์ž‘์—… ์ˆ˜ํ–‰
        }
    }

    protected Chunk<O> transform(StepContribution contribution, Chunk<I> inputs) throws Exception {
        Chunk<O> outputs = new Chunk<>();
        for (Chunk<I>.ChunkIterator iterator = inputs.iterator(); iterator.hasNext();) {
            final I item = iterator.next();
            O output;
            Timer.Sample sample = BatchMetrics.createTimerSample(this.meterRegistry);
            String status = BatchMetrics.STATUS_SUCCESS;
            try {
                output = doProcess(item); // ํ•˜๋‚˜์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณตํ•˜๊ธฐ ์œ„ํ•œ ํ˜ธ์ถœ 
            }
            catch (Exception e) {
                inputs.clear();
                status = BatchMetrics.STATUS_FAILURE;
                throw e;
            }
            finally {
                stopTimer(sample, contribution.getStepExecution(), "item.process", status, "Item processing");
            }
            
            if (output != null) {
                outputs.add(output);
            }
            else {
                iterator.remove();
            }
        }
        if (inputs.isEnd()) {
            outputs.setEnd();
        }
        return outputs;
    }
    
    protected final O doProcess(I item) throws Exception {
        Object result;
        if (this.itemProcessor == null) {
            result = item;
            return result;
        } else {
            try {
                this.listener.beforeProcess(item);
                result = this.itemProcessor.process(item); // 1. Step ์ •์˜ํ•  ๋•Œ, ์ž‘์„ฑํ•œ process() ๋กœ์ง ํ˜ธ์ถœ
                this.listener.afterProcess(item, result);
                return result;
            } catch (Exception var3) {
                Exception e = var3;
                this.listener.onProcessError(item, e);
                throw e;
            }
        }
    }

    protected void write(StepContribution contribution, Chunk<I> inputs, Chunk<O> outputs) throws Exception {
        Timer.Sample sample = BatchMetrics.createTimerSample(this.meterRegistry);
        String status = BatchMetrics.STATUS_SUCCESS;
        try {
            doWrite(outputs);
        }
        catch (Exception e) {
            inputs.clear();
            status = BatchMetrics.STATUS_FAILURE;
            throw e;
        }
        finally {
            stopTimer(sample, contribution.getStepExecution(), "chunk.write", status, "Chunk writing");
        }
        contribution.incrementWriteCount(outputs.size());
    }

    protected final void doWrite(Chunk<O> items) throws Exception {
        // ...
        try {
            listener.beforeWrite(items);
            writeItems(items);
            doAfterWrite(items);
        }
        catch (Exception e) {
            doOnWriteError(e, items);
            throw e;
        }
    }

    protected void writeItems(Chunk<O> items) throws Exception {
        if (itemWriter != null) {
            itemWriter.write(items); // 2. Step ์ •์˜ํ•  ๋•Œ, ์ž‘์„ฑํ•œ write() ๋กœ์ง ํ˜ธ์ถœ
        }
    }
}

process()๋ฉ”์„œ๋“œ ํ˜ธ์ถœ์„ ํ†ตํ•ด inputs ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›์•„, ๋‚ด๋ถ€์ ์œผ๋กœ Processor ๋กœ์ง๊ณผ Writer ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. ๊ฐ๊ฐ transform(), write() ๋ฉ”์„œ๋“œ๊ฐ€ ์ด์— ํ•ด๋‹นํ•œ๋‹ค.

  1. transform() chunk size ๋งŒํผ ํŠน์ • ํƒ€์ž…์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณตํ•ด์•ผ ํ•œ๋‹ค. doProcess()์— ๋‹จ์ผ ๋ฐ์ดํ„ฐ๋ฅผ ๋„˜๊ฒจ, Step์„ ์ •์˜ํ•  ๋•Œ ์ž‘์„ฑํ–ˆ๋˜ ItemProcessor ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

  2. write() transform()์„ ํ†ตํ•ด ๊ฐ€๊ณต๋œ ๋ฐ์ดํ„ฐ๋ฅผ DB์— ์“ฐ๊ธฐ ์œ„ํ•ด, ํ˜ธ์ถœํ•œ๋‹ค. ์ด๋Š” ๋ฐ˜๋ณต๋ฌธ์„ ํ†ตํ•ด ์ˆ˜ํ–‰๋˜๋Š”๊ฒŒ ์•„๋‹Œ ๊ฐ€๊ณต๋œ ๋ฐ์ดํ„ฐ ๋ฆฌ์ŠคํŠธ(Chunk<O> items)๋ฅผ ํ•œ๋ฒˆ์— ์“ด๋‹ค.


์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ChunkOrientedTasklet์˜ ์ƒ์„ฑ๊ณผ ์‹คํ–‰ ๋ฐฉ์‹์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์•˜๋‹ค. ์•Œ์•„๋ณด๊ธฐ์— ์•ž์„œ Tasklet::execute()๋ฅผ ์ปค์Šคํ…€ํ•˜๊ฒŒ ์ •์˜ํ•œ ๋ฐฉ์‹๊ณผ์˜ ์ฐจ์ด๋„ ์•Œ์•„๋ณด์•˜๋‹ค.