ItemReader

chunk

Spring Batch์˜ Reader์—์„œ ์ฝ์–ด์˜ฌ ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ ์œ ํ˜•์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • ์ž…๋ ฅ ๋ฐ์ดํ„ฐ์—์„œ ์ฝ์–ด์˜ค๊ธฐ

  • ํŒŒ์ผ์—์„œ ์ฝ์–ด์˜ค๊ธฐ

  • DB์—์„œ ์ฝ์–ด์˜ค๊ธฐ

  • Java Message Service ๋“ฑ ๋‹ค๋ฅธ ์†Œ์Šค์—์„œ ์ฝ์–ด์˜ค๊ธฐ

  • ์ปค์Šคํ…€ํ•œ Reader๋กœ ์ฝ์–ด์˜ค๊ธฐ

package org.springframework.batch.item;

public interface ItemReader<T> {

	@Nullable
	T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException;

}

ItemReader์˜ read()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด, ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋Š” ์Šคํ… ๋‚ด์—์„œ ์ฒ˜๋ฆฌํ•  Itemํ•œ๊ฐœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ, ์Šคํ…์—์„œ๋Š” ์•„์ดํ…œ ๊ฐœ์ˆ˜๋ฅผ ์„ธ์–ด ์ฒญํฌ ๋‚ด ๋ฐ์ดํ„ฐ๊ฐ€ ๋ช‡๊ฐœ๊ฐ€ ์ฒ˜๋ฆฌ๋๋Š”์ง€ ๊ด€๋ฆฌํ•œ๋‹ค. ํ•ด๋‹น Item์€ ItemProcessor๋กœ ์ „๋‹ฌ๋˜๋ฉฐ, ๊ทธ ๋’ค ItemWriter๋กœ ์ „๋‹ฌ๋œ๋‹ค.

๊ฐ€์žฅ ๋Œ€ํ‘œ์ ์ธ ๊ตฌํ˜„์ฒด์ธ JdbcPagingItemReader์˜ ํด๋ž˜์Šค ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

image-20210209153930033

์—ฌ๊ธฐ์„œ ItemReader์™€ ItemStream ์ธํ„ฐํŽ˜์ด์Šค๋„ ๊ฐ™์ด ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

public interface ItemStream {
    void open(ExecutionContext var1) throws ItemStreamException;

    // Batch์˜ ์ฒ˜๋ฆฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
    void update(ExecutionContext var1) throws ItemStreamException;

    void close() throws ItemStreamException;
}

ItemStream์€ ์ฃผ๊ธฐ์ ์œผ๋กœ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๊ณ , ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ํ•ด๋‹น ์ƒํƒœ์—์„œ ๋ณต์›ํ•˜๊ธฐ ์œ„ํ•œ ๋งˆ์ปค์ธํ„ฐํŽ˜์ด์Šค์ด๋‹ค. ์ฆ‰, ItemReader ์˜ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๊ณ  ์‹คํŒจํ•œ ๊ณณ์—์„œ ๋‹ค์‹œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. ItemReader์™€ ItemStream ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•˜์—ฌ ์›ํ•˜๋Š” ํ˜•ํƒœ์˜ ItemReader๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

ํŒŒ์ผ ์ž…๋ ฅ

FlatFileItemReader

  • org.springframework.batch.item.file.FlatFileItemReader

  • flat file

    • ํ•œ ๊ฐœ ํ˜น์€ ๊ทธ ์ด์ƒ์˜ ๋ ˆ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋œ ํŠน์ • ํŒŒ์ผ

    • ํŒŒ์ผ์˜ ๋‚ด์šฉ์„ ๋ด๋„ ๋ฐ์ดํ„ฐ์˜ ์˜๋ฏธ๋ฅผ ์•Œ ์ˆ˜ ์—†๋‹ค.

    • ํŒŒ์ผ ๋‚ด ๋ฐ์ดํ„ฐ์˜ ํฌ๋งท์ด๋‚˜ ์˜๋ฏธ๋ฅผ ์ •์˜ํ•˜๋Š” ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋‹ค.

์˜ต์…˜
ํƒ€์ž…
default
์„ค๋ช…
๊ตฌํ˜„์ฒด

comments

String[]

null

๋ฌธ์ž์—ด ๋ฐฐ์—ด์— ํŒŒ์ผ์„ ํŒŒ์‹ฑํ•  ๋–„ ๊ฑด๋„ˆ๋›ฐ์–ด์•ผํ•  ์ฃผ์„ ์ค„์„ ๋‚˜ํƒ€๋‚ด๋Š” ์ ‘๋‘์–ด ์ง€์ •

encoding

String

ํ”Œ๋žซํผ์˜ ๊ธฐ๋ณธ Charset

ํŒŒ์ผ์— ์‚ฌ์šฉ๋œ ๋ฌธ์ž์—ด ์ธ์ฝ”๋”ฉ

lineMapper

LineMapper

null(ํ•„์ˆ˜)

ํŒŒ์ผ ํ•œ์ค„์„ String์œผ๋กœ ์ฝ์€ ๋’ค ์ฒ˜๋ฆฌ ๋Œ€์ƒ์ธ ๋„๋ฉ”์ธ ๊ฐ์ฒด(Item)์œผ๋กœ ๋ณ€ํ™˜

DefaultLineMapper JsonLineMapper PassThroughLineMapper

lineToSkip

int

0

ํŒŒ์ผ์„ ์ฝ์–ด์˜ฌ ๋–„ ๋ช‡ ์ค„์„ ๊ฑด๋„ˆ๋„๊ณ  ์‹œ์ž‘ํ• ์ง€ ์ง€์ •

recordSeparatorPolicy

RecordSeparatorPolicy

DefaultRecordSeparatorPolicy

๊ฐ ์ค„์˜ ๋งˆ์ง€๋ง‰์„ ์ •์˜ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ ๋ณ„๋„๋กœ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฐœํ–‰ ๋ฌธ์ž๊ฐ€ ๋ ˆ์ฝ”๋“œ์˜ ๋ ๋ถ€๋ถ„์„ ๋‚˜ํƒ€๋‚ธ๋‹ค.

resource

Resource

null(ํ•„์ˆ˜)

์ฝ์„ ๋Œ€์ƒ ๋ฆฌ์†Œ์Šค

skippedLinesCallback

LineCallbackHandler

null

์ค„์„ ๊ฑด๋„ˆ๋›ธ ๋–„ ํ˜ธ์ถœ๋˜๋Š” ์ฝœ๋ฐฑ ์ธํ„ฐํŽ˜์ด์Šค ๊ฑด๋„ˆ๋ˆ ๋ชจ๋“  ์ค„์€ ์ด ์ฝœ๋ฐฑ์ด ํ˜ธ์ถœ๋œ๋‹ค.

strict

boolean

false

true๋กœ ์ง€์ •์‹œ, ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ Exception์„ ๋˜์ง„๋‹ค.

saveState

boolean

true

true : ์žฌ์‹œ์ž‘ ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ฐ ์ฒญํฌ ์ฒ˜๋ฆฌ ํ›„ ItemReader ์ƒํƒœ ์ €์žฅ false : ๋‹ค์ค‘ ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„  false ์ง€์ •

๊ณ ์ •๋œ ๋„ˆ๋น„ ํŒŒ์ผ

Aimee      CHoover    7341Vel Avenue          Mobile          AL35928
Jonas      UGilbert   8852In St.              Saint Paul      MN57321
Regan      MBaxter    4851Nec Av.             Gulfport        MS33193
Octavius   TJohnson   7418Cum Road            Houston         TX51507
Sydnee     NRobinson  894 Ornare. Ave         Olathe          KS25606
Stuart     KMckenzie  5529Orci Av.            Nampa           ID18562
    @Bean
    @StepScope
    public FlatFileItemReader<Customer> customerItemReader(@Value("#{jobParameters['customerFile']}") PathResource inputFile) {
        return new FlatFileItemReaderBuilder<Customer>()
                .name("customerItemReader") // ๊ฐ ์Šคํ…์˜ ExecutionContext์— ์ถ”๊ฐ€๋˜๋Š” ํŠน์ •ํ‚ค์˜ ์ ‘๋‘๋ฌธ์ž๋กœ ์‚ฌ์šฉ๋  ์ด๋ฆ„(saveState false์ธ ๊ฒฝ์šฐ ์ง€์ •ํ•  ํ•„์š”X)
                .resource(inputFile)
                .fixedLength() // FixedLengthBuilder
                .columns(new Range[]{new Range(1,11), new Range(12,12), new Range(13,22), new Range(23,26)
                        , new Range(27,46), new Range(47,62), new Range(63,64), new Range(65,69)}) // ๊ณ ์ •๋„ˆ๋น„
                .names(new String[]{"firstName", "middleInitial", "lastName", "addressNumber", "street"
                        , "city", "state", "zipCode"}) // ๊ฐ ์ปฌ๋Ÿผ๋ช…
//                .strict(false) // ์ •์˜๋œ ํŒŒ์‹ฑ ์ •๋ณด ๋ณด๋‹ค ๋งŽ์€ ํ•ญ๋ชฉ์ด ๋ ˆ์ฝ”๋“œ์— ์žˆ๋Š” ๊ฒฝ์šฐ(true ์˜ˆ์™ธ)
                .targetType(Customer.class) // BeanWrapperFieldSetMapper ์ƒ์„ฑํ•ด ๋„๋ฉ”์ธ ํด๋ ˆ์Šค์— ๊ฐ’์„ ์ฑ„์›€
                .build();
    }

๊ตฌ๋ถ„์ž ํŒŒ์ผ

Aimee,C,Hoover,7341,Vel Avenue,Mobile,AL,35928
Jonas,U,Gilbert,8852,In St.,Saint Paul,MN,57321
Regan,M,Baxter,4851,Nec Av.,Gulfport,MS,33193
Octavius,T,Johnson,7418,Cum Road,Houston,TX,51507
Sydnee,N,Robinson,894,Ornare. Ave,Olathe,KS,25606
Stuart,K,Mckenzie,5529,Orci Av.,Nampa,ID,18562
    @Bean
    @StepScope
    public FlatFileItemReader<Customer> delimitedCustomerItemReader(@Value("#{jobParameters['customerFile']}") PathResource inputFile) {
        return new FlatFileItemReaderBuilder<Customer>()
                .name("delimitedCustomerItemReader") // ๊ฐ ์Šคํ…์˜ ExecutionContext์— ์ถ”๊ฐ€๋˜๋Š” ํŠน์ •ํ‚ค์˜ ์ ‘๋‘๋ฌธ์ž๋กœ ์‚ฌ์šฉ๋  ์ด๋ฆ„(saveState false์ธ ๊ฒฝ์šฐ ์ง€์ •ํ•  ํ•„์š”X)
                .resource(inputFile)
                .delimited() // default(,) DelimitedLineTokenizer๋ฅผ ์‚ฌ์šฉํ•ด ๊ฐ ๋ ˆ์ฝ”๋“œ๋ฅผ FieldSet์œผ๋กœ ๋ณ€ํ™˜
                .names(new String[]{"firstName", "middleInitial", "lastName", "addressNumber", "street"
                        , "city", "state", "zipCode"}) // ๊ฐ ์ปฌ๋Ÿผ๋ช…
                .targetType(Customer.class) // BeanWrapperFieldSetMapper ์ƒ์„ฑํ•ด ๋„๋ฉ”์ธ ํด๋ ˆ์Šค์— ๊ฐ’์„ ์ฑ„์›€
                .build();
    }

FieldSetMapper ์ปค์Šคํ…€

public interface FieldSetMapper<T> {
	T mapFieldSet(FieldSet fieldSet) throws BindException;
}

org.springframework.batch.item.file.mapping.FieldSetMapper๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ์ปค์Šคํ…€ ๋งคํผ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

public class CustomFieldSetMapper implements FieldSetMapper<Customer> {

    @Override
    public Customer mapFieldSet(FieldSet fieldSet) throws BindException {
        Customer customer = new Customer();
        customer.setAddress(fieldSet.readString("addressNumber") + " " + fieldSet.readString("street"));
        customer.setCity(fieldSet.readString("city"));
        customer.setFirstName(fieldSet.readString("firstName"));
        customer.setLastName(fieldSet.readString("lastName"));
        customer.setMiddleInitial(fieldSet.readString("middleInitial"));
        customer.setState(fieldSet.readString("state"));
        customer.setZipCode(fieldSet.readString("zipCode"));

        return customer;
    }
}
    @Bean
    @StepScope
    public FlatFileItemReader<Customer> delimitedCustomerItemReader(@Value("#{jobParameters['customerFile']}") PathResource inputFile) {
        return new FlatFileItemReaderBuilder<Customer>()
                .name("delimitedCustomerItemReader")
                .resource(inputFile)
                .delimited() 
                .names(new String[]{"firstName", "middleInitial", "lastName", 														"addressNumber", "street", "city", "state", "zipCode"})
                .fieldSetMapper(new CustomFieldSetMapper()) // customMapper ์„ค์ •
                .build();
    }

.fieldSetMapper()์— ์ปค์Šคํ…€ ๋งคํผ๋ฅผ ์ง€์ •ํ•˜๋ฉด ๋œ๋‹ค.

LineTokenizer ์ปค์Šคํ…€

  • org.springframework.batch.item.file.transform.LineTokenizer

public interface LineTokenizer {
	FieldSet tokenize(@Nullable String line);
}
public class CustomFileLineTokenizer implements LineTokenizer {

    @Setter
    private String delimiter = ",";

    private String[] names = new String[]{
              "firstName"
            , "middleInitial"
            , "lastName"
            , "address"
            , "city"
            , "state"
            , "zipCode"
    };

    private FieldSetFactory fieldSetFactory = new DefaultFieldSetFactory();

    @Override
    public FieldSet tokenize(String line) {

        // ๊ตฌ๋ถ„์ž๋กœ ํ•„๋“œ ๊ตฌ๋ถ„
        String[] fields = line.split(delimiter);

        List<String> parsedFields = new ArrayList<>();

        for (int i = 0; i < fields.length; i++) {
            if (i == 4) {
                // 3,4๋ฒˆ์จฐ ํ•„๋“œ ๋‹จ์ผ ํ•„๋“œ๋กœ ๊ตฌ์„ฑ
                parsedFields.set(i - 1, parsedFields.get(i - 1) + " " + fields[i]);
            } else {
                parsedFields.add(fields[i]);
            }
        }


        // ๊ฐ’์˜ ๋ฐฐ์—ด & ํ•„๋“œ ์ด๋ฆ„ ๋ฐฐ์—ด์„ ๋„˜๊ฒจ ํ•„๋“œ๋ฅผ ์ƒ์„ฑ
        return fieldSetFactory.create(parsedFields.toArray(new String[0]), names);
    }
}
    @Bean
    @StepScope
    public FlatFileItemReader<Customer> lineTokenizerCustomerItemReader(@Value("#{jobParameters['customerFile']}") PathResource inputFile) {
        return new FlatFileItemReaderBuilder<Customer>()
                .name("lineTokenizerCustomerItemReader")
                .resource(inputFile)
                .lineTokenizer(new CustomFileLineTokenizer()) // lineTokenzier Custom
                .targetType(Customer.class) // BeanWrapperFieldSetMapper ์ƒ์„ฑํ•ด ๋„๋ฉ”์ธ ํด๋ ˆ์Šค์— ๊ฐ’์„ ์ฑ„์›€
                .build();
    }

LineMapper

  • org.springframework.batch.item.file.mapping.PatternMatchingCompositeLineMapper

    • ์—ฌ๋Ÿฌ๊ฐœ์˜ LineTokenizer๋กœ ๊ตฌ์„ฑ๋œ Map์„ ์„ ์–ธํ•  ์ˆ˜ ์žˆ์Œ.

      • PatternMatcher<LineTokenizer> tokenizers

    • ๊ฐ LineTokenizer๋ฅผ ํ•„์š”๋กœ ํ•˜๋Š” ์—ฌ๋Ÿฌ๊ฐœ์˜ FieldSetMapper Map ์„ ์–ธํ•  ์ˆ˜ ์žˆ์Œ.

      • PatternMatcher<FieldSetMapper<T>> patternMatcher

CUST,Warren,Q,Darrow,8272 4th Street,New York,IL,76091
TRANS,1165965,2011-01-22 00:13:29,51.43
CUST,Ann,V,Gates,9247 Infinite Loop Drive,Hollywood,NE,37612
CUST,Erica,I,Jobs,8875 Farnam Street,Aurora,IL,36314
TRANS,8116369,2011-01-21 20:40:52,-14.83
TRANS,8116369,2011-01-21 15:50:17,-45.45
TRANS,8116369,2011-01-21 16:52:46,-74.6
TRANS,8116369,2011-01-22 13:51:05,48.55
TRANS,8116369,2011-01-21 16:51:59,98.53

์—ฌ๋Ÿฌ ํ˜•์‹์œผ๋กœ ๊ตฌ์„ฑ๋œ csv ํŒŒ์ผ์ด๋‹ค.

		@Bean
    public PatternMatchingCompositeLineMapper lineTokenizer() {
        Map<String, LineTokenizer> lineTokenizerMap = new HashMap<>(2);

        lineTokenizerMap.put("TRANS*", transactionLineTokenizer()); // TRANS๋กœ ์‹œ์ž‘ํ•˜๋ฉด transactionLineTokenizer
        lineTokenizerMap.put("CUST*", customerLineTokenizer()); // CUST๋กœ ์‹œ์ž‘ํ•˜๋ฉด, customerLineTokenizer

        Map<String, FieldSetMapper> fieldSetMapperMap = new HashMap<>(2);

        BeanWrapperFieldSetMapper<Customer> customerFieldSetMapper = new BeanWrapperFieldSetMapper<>();
        customerFieldSetMapper.setTargetType(Customer.class);

        fieldSetMapperMap.put("TRANS*", new TransactionFieldSetMapper()); // ์ผ๋ฐ˜์ ์ด์ง€ ์•Š์€ ํƒ€์ž… ํ•„๋“œ ๋ณ€ํ™˜์‹œ FieldSetMapper ํ•„์š”(Date, Double)
        fieldSetMapperMap.put("CUST*", customerFieldSetMapper);

        PatternMatchingCompositeLineMapper lineMappers = new PatternMatchingCompositeLineMapper();

        lineMappers.setTokenizers(lineTokenizerMap);
        lineMappers.setFieldSetMappers(fieldSetMapperMap);

        return lineMappers;
    }

TRANS๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ์™€ CUST๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ ๊ฐ๊ฐ FieldSetMapper, LineTokenizer๋ฅผ ์‚ฌ์šฉํ•ด ํŒŒ์‹ฑ ๋ฐ set์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

    @Bean
    public DelimitedLineTokenizer transactionLineTokenizer() {
        DelimitedLineTokenizer delimitedLineTokenizer = new DelimitedLineTokenizer();

        delimitedLineTokenizer.setNames("prefix", "accountNumber", "transactionDate", "amount");

        return delimitedLineTokenizer;
    }

    @Bean
    public DelimitedLineTokenizer customerLineTokenizer() {
        DelimitedLineTokenizer delimitedLineTokenizer = new DelimitedLineTokenizer();

        delimitedLineTokenizer.setNames("firstName", "middleInitial", "lastName", "address", "city", "state", "zipCode");
        delimitedLineTokenizer.setIncludedFields(1,2,3,4,5,6,7); // prefix์ œ์™ธํ•œ ๋ชจ๋“  ํ•„๋“œ

        return delimitedLineTokenizer;
    }

ItemStreamReader ์ปค์Šคํ…€

๋‘๊ฐœ์˜ ๋‹ค๋ฅธ ํฌ๋งท์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์‚ฌ์‹ค์€ ์„œ๋กœ ์—ฐ๊ด€์ด ์žˆ๋Š” ๋ฐ์ดํ„ฐ์ผ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ ๊ฒฝ์šฐ์—๋Š” ํ•œ๊ฐœ์˜ ๋„๋ฉ”์ธ์ด ๋‹ค๋ฅธ ํ•œ๊ฐœ์˜ ๋„๋ฉ”์ธ์˜ ๋‚ด์šฉ์„ ํฌํ•จํ•˜๊ณ  ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค.

CUST,Warren,Q,Darrow,8272 4th Street,New York,IL,76091
TRANS,1165965,2011-01-22 00:13:29,51.43
CUST,Ann,V,Gates,9247 Infinite Loop Drive,Hollywood,NE,37612
CUST,Erica,I,Jobs,8875 Farnam Street,Aurora,IL,36314
TRANS,8116369,2011-01-21 20:40:52,-14.83
TRANS,8116369,2011-01-21 15:50:17,-45.45
TRANS,8116369,2011-01-21 16:52:46,-74.6
TRANS,8116369,2011-01-22 13:51:05,48.55
TRANS,8116369,2011-01-21 16:51:59,98.53

๊ฑฐ๋ž˜๋‚ด์—ญ(TRANS) ๋ฐ์ดํ„ฐ๋Š” ๊ทธ ์œ„์˜ ๊ณ ๊ฐ(CUST)์˜ ๊ณ„์•ฝ ์ •๋ณด๋ผ๊ณ  ๊ฐ€์ •ํ•ด๋ณผ ๊ฒƒ์ด๋‹ค.

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class Customer {

    private Long id;
    private String firstName;
    private String middleInitial;
    private String lastName;
    private String addressNumber;
		private String street;
    private String city;
    private String state;
    private String zipCode;

    private String address;

    private List<Transaction> transactions; // ๊ณ ๊ฐ์˜ ๊ฐœ์•ฝ์ •๋ณด

}

Custom ๋„๋ฉ”์ธ ๊ฐ์ฒด์— Transaction ๊ฑฐ๋ž˜๋‚ด์—ญ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๊ฒŒ ๋ณ€๊ฒฝํ•ด์ค€๋‹ค.

public class CustomerFileReader implements ItemStreamReader<Customer> {

    private Object curItem = null;

    private ItemStreamReader<Object> delegate;

    public CustomerFileReader(ItemStreamReader<Object> delegate) {
        this.delegate = delegate;
    }

    @Override
    public Customer read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
        if (curItem == null) {
            curItem = delegate.read(); // ๊ณ ๊ฐ ์ •๋ณด๋ฅผ ์ฝ์Œ.
        }

        Customer item = (Customer) curItem;
        curItem = null;

        if (item != null) {
            item.setTransactions(new ArrayList<>());

            // ๋‹ค์Œ ๊ณ ๊ฐ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋งŒ๋‚˜๊ธฐ ์ „๊นŒ์ง€ ๊ฑฐ๋ž˜๋‚ด์—ญ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ฝ๋Š”๋‹ค.
            while (peek() instanceof Transaction) {
                item.getTransactions().add((Transaction) curItem);
                curItem = null;
            }
        }

        return item;
    }

    private Object peek() throws Exception {
        if (curItem == null) {
            curItem = delegate.read();
        }
        return curItem;
    }



    @Override
    public void open(ExecutionContext executionContext) throws ItemStreamException {
        delegate.open(executionContext);
    }

    @Override
    public void update(ExecutionContext executionContext) throws ItemStreamException {
        delegate.update(executionContext);
    }

    @Override
    public void close() throws ItemStreamException {
        delegate.close();
    }
}

์—ฌ๊ธฐ์„œ ํ•ต์‹ฌ์€ read() ๋ฉ”์„œ๋“œ์ด๋‹ค. ํ•œ์ค„์”ฉ ์ฝ์–ด์˜ฌ ๋•Œ ๋‹ค์Œ ๊ณ ๊ฐ์ •๋ณด๊ฐ€ ๋‚˜์˜ฌ๋•Œ๊นŒ์ง€ ๊ฑฐ๋ž˜๋‚ด์—ญ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ฝ์–ด ํ•ด๋‹น ๊ณ ๊ฐ์ด ๊ฐ€์ง€๊ณ  ์žˆ๊ฒŒ ํ•œ๋‹ค.

    @Bean
    @StepScope
    public FlatFileItemReader multiLineItemReader(@Value("#{jobParameters['customFile']}") PathResource resource) {
        return new FlatFileItemReaderBuilder<Customer>()
                .name("multiLineItemReader")
                .lineMapper(multiLineTokenizer())
                .resource(resource)
                .build();
    }

    @Bean
    public CustomerFileReader customerFileReader() {
        return new CustomerFileReader(multiLineItemReader(null));
    }

Job์—์„œ itemReader๋ถ€๋ถ„์— ์œ„์—์„œ ์ƒ์„ฑํ•œ CustomerFileReader๋ฅผ ์„ค์ •ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

Customer(id=null, firstName=Warren, middleInitial=Q, lastName=Darrow, addressNumber=null, street=null, city=New York, state=IL, zipCode=76091, address=8272 4th Street, transactions=[Transaction(accountNumber=1165965, transactionDate=Sat Jan 22 00:13:29 KST 2011, amount=51.43, dateFormat=java.text.SimpleDateFormat@7c669100)])
Customer(id=null, firstName=Ann, middleInitial=V, lastName=Gates, addressNumber=null, street=null, city=Hollywood, state=NE, zipCode=37612, address=9247 Infinite Loop Drive, transactions=[])
Customer(id=null, firstName=Erica, middleInitial=I, lastName=Jobs, addressNumber=null, street=null, city=Aurora, state=IL, zipCode=36314, address=8875 Farnam Street, transactions=[Transaction(accountNumber=8116369, transactionDate=Fri Jan 21 20:40:52 KST 2011, amount=-14.83, dateFormat=java.text.SimpleDateFormat@7c669100), Transaction(accountNumber=8116369, transactionDate=Fri Jan 21 15:50:17 KST 2011, amount=-45.45, dateFormat=java.text.SimpleDateFormat@7c669100), Transaction(accountNumber=8116369, transactionDate=Fri Jan 21 16:52:46 KST 2011, amount=-74.6, dateFormat=java.text.SimpleDateFormat@7c669100), Transaction(accountNumber=8116369, transactionDate=Sat Jan 22 13:51:05 KST 2011, amount=48.55, dateFormat=java.text.SimpleDateFormat@7c669100), Transaction(accountNumber=8116369, transactionDate=Fri Jan 21 16:51:59 KST 2011, amount=98.53, dateFormat=java.text.SimpleDateFormat@7c669100)])

๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ณ ๊ฐ์ด ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฑฐ๋ž˜๋‚ด์—ญ์„ ์ถœ๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค.

MultiResourceItemReader

๋™์ผํ•œ ํฌ๋งท์œผ๋กœ ์ž‘์„ฑ๋œ ์—ฌ๋Ÿฌ๊ฐœ์˜ ํŒŒ์ผ์„ ์ฝ์–ด๋“ค์ด๋Š” ItemReader๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

  • org.springframework.batch.item.file.MultiResourceItemReader

MultiResourceItemReader๋Š” ์ฝ์–ด์•ผํ•  ํŒŒ์ผ๋ช…์˜ ํŒจํ„ด์„ MultiResourceItemReader์˜ ์˜์กด์„ฑ์œผ๋กœ ์ •์˜ํ•œ๋‹ค.

public class MultiResourceCustomerFileReader implements ResourceAwareItemReaderItemStream<Customer> {

    private Object curItem = null;

    private ResourceAwareItemReaderItemStream<Object> delegate;

    public MultiResourceCustomerFileReader(ResourceAwareItemReaderItemStream<Object> delegate) {
        this.delegate = delegate;
    }


    /**
     * Resource๋ฅผ ์ฃผ์ž…ํ•จ์œผ๋กœ ItemReader๊ฐ€ ํŒŒ์ผ ๊ด€๋ฆฌํ•˜๋Š” ๋Œ€์‹ 
     * ๊ฐ ํŒŒ์ผ์„ ์Šคํ”„๋ง ๋ฐฐ์น˜๊ฐ€ ์ƒ์„ฑํ•ด ์ฃผ์ž…
     * @param resource
     */
    @Override
    public void setResource(Resource resource) {
        System.out.println(resource);
        this.delegate.setResource(resource);
    }

    @Override
    public Customer read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
        if (curItem == null) {
            curItem = delegate.read(); // ๊ณ ๊ฐ ์ •๋ณด๋ฅผ ์ฝ์Œ.
        }

        Customer item = (Customer) curItem;
        curItem = null;

        if (item != null) {
            item.setTransactions(new ArrayList<>());

            // ๋‹ค์Œ ๊ณ ๊ฐ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋งŒ๋‚˜๊ธฐ ์ „๊นŒ์ง€ ๊ฑฐ๋ž˜๋‚ด์—ญ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ฝ๋Š”๋‹ค.
            while (peek() instanceof Transaction) {
                item.getTransactions().add((Transaction) curItem);
                curItem = null;
            }
        }

        return item;
    }

    private Object peek() throws Exception {
        if (curItem == null) {
            curItem = delegate.read();
        }
        return curItem;
    }

    @Override
    public void open(ExecutionContext executionContext) throws ItemStreamException {
        delegate.open(executionContext);
    }

    @Override
    public void update(ExecutionContext executionContext) throws ItemStreamException {
        delegate.update(executionContext);
    }

    @Override
    public void close() throws ItemStreamException {
        delegate.close();
    }
}

์œ„์—์„œ ๋‹ค๋ฃฌ ItemStreamReader ์™€ ๋‹ค๋ฅธ ์ ์€ Resource ์ฃผ์ž…๋ถ€๋ถ„์ด๋‹ค. Resource๋ฅผ ์ฃผ์ž…ํ•˜๊ฒŒ ๋˜๋ฉด ํ•„์š”ํ•œ ๊ฐ ํŒŒ์ผ์„ ์Šคํ”„๋ง ๋ฐฐ์น˜๊ฐ€ ์ƒ์„ฑํ•ด ItemReader์— ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค.

@Bean
    @StepScope
    public MultiResourceItemReader multiResourceItemReader(@Value("#{jobParameters['customFile']}") Resource[] resources) {
        return new MultiResourceItemReaderBuilder<>()
                .name("multiResourceItemReader")
                .resources(resources) // resources ๋ฐฐ์—ด
                .delegate(multiResourceCustomerFileReader()) // ์‹ค์ œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์œ„์ž„ ์ปดํฌ๋„ŒํŠธ
                .build();
    }

    @Bean
    public MultiResourceCustomerFileReader multiResourceCustomerFileReader() {
        return new MultiResourceCustomerFileReader(multiResourceCustomerItemReader());
    }

    @Bean
    @StepScope
    public FlatFileItemReader multiResourceCustomerItemReader() {
        return new FlatFileItemReaderBuilder<Customer>()
                .name("multiResourceCustomerItemReader")
                .lineMapper(multiResourceTokenizer())
                .build();
    }

์ฝ์–ด์•ผํ•  ํŒŒ์ผ ๋ชฉ๋ก(resources)์„ ์„ค์ •ํ•ด์ฃผ๊ณ , delegate()์— ์‹ค์ œ๋กœ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์œ„์ž„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง€์ •ํ•ด์ฃผ๋ฉด๋œ๋‹ค.

์—ฌ๋Ÿฌ ๊ฐœ์˜ ํŒŒ์ผ์„ ๋‹ค๋ฃฐ๋•Œ๋Š” ์žฌ์‹œ์ž‘์„ ํ•˜๊ฒŒ๋˜๋Š” ์ƒํ™ฉ์—์„œ ์Šคํ”„๋ง๋ฐฐ์น˜๊ฐ€ ์ถ”๊ฐ€์ ์ธ ์•ˆ์ •์žฅ์น˜๋ฅผ ์ œ๊ณตํ•ด์ฃผ์ง€ ์•Š๋Š”๋‹ค. ์˜ˆ๋ฅผ๋“ค์–ด file1.csv, file2.csv, file3.csv๊ฐ€ ์žˆ๋Š”๋ฐ, file2.csv ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณผ์ •์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ์žก์ด ์‹คํŒจ ๋œ ์ดํ›„ ์žฌ์‹œ์ž‘์„ ํ• ๋•Œ file4.csv๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค๋ฉด, ์ตœ์ดˆ ์‹คํ–‰์‹œ file4.csv๊ฐ€ ์—†์—ˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ , ํฌํ•จํ•˜์—ฌ ์‹คํ–‰ํ•œ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฌธ์ œ์ ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋ฐฐ์น˜ ์‹คํ–‰ ์‹œ ์‚ฌ์šฉํ•  ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ๋ณ„๋„๋กœ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ด๋ฉฐ, ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ๋ชจ๋“  ํŒŒ์ผ์€ ์ƒˆ๋กœ์šด ๋””๋ ‰ํ„ฐ๋ฆฌ์— ๋„ฃ์–ด์ฃผ์–ด ํ˜„์žฌ ์ˆ˜ํ–‰์ค‘์ธ ์žก์— ์˜ํ–ฅ์„ ์ฃผ์ง€์•Š๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค.

XML

XML์€ ํŒŒ์ผ ๋‚ด ๋ฐ์ดํ„ฐ๋ฅผ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋Š” ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•ด ํŒŒ์ผ์— ํฌํ•จ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์„ค๋ช…ํ•˜๋ฏ€๋กœ, Flat file๊ณผ๋Š” ๋‹ค๋ฅด๋‹ค.

XML parser๋กœ ์ฃผ๋กœ DOM๊ณผ SAX๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•œ๋‹ค.

Dom vs SAX vs StAX

DOM(Document Object Model) ๋ฐฉ์‹

  • XML๋ฌธ์„œ ์ „์ฒด๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ๋กœ๋“œํ•˜์—ฌ ๊ฐ’์„ ์ฝ๋Š”๋‹ค.

  • XML๋ฌธ์„œ๋ฅผ ์ฝ์œผ๋ฉด ๋ชจ๋“  Element, Text, Attribute ๋“ฑ์— ๋Œ€ํ•œ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ Document ๊ฐ์ฒด๋กœ ๋ฆฌํ„ดํ•œ๋‹ค.

  • Document ๊ฐ์ฒด๋Š” DOM API์— ์•Œ๋งž๋Š” ํŠธ๋ฆฌ ๊ตฌ์กฐ์˜ ์ž๋ฐ” ๊ฐ์ฒด๋กœ ํ‘œํ˜„๋˜์–ด ์žˆ๋‹ค.

  • XML๋ฌธ์„œ๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์— ๋ชจ๋‘ ์˜ฌ๋ผ๊ฐ€ ์žˆ์–ด์„œ ๋…ธ๋“œ๋“ค์˜ ๊ฒ€์ƒ‰, ์ˆ˜์ •, ๊ตฌ์กฐ๋ณ€๊ฒฝ์ด ๋น ๋ฅด๊ณ  ์šฉ์ดํ•˜๋‹ค.

  • SAX ๋ฐฉ์‹ ๋ณด๋‹ค ์ง๊ด€์ ์ด๋ฉฐ ํŒŒ์‹ฑ์ด ๋‹จ์ˆœํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ผ๋ฐ˜์ ์œผ๋กœ DOM ๋ฐฉ์‹์„ ์ฑ„ํƒํ•˜์—ฌ ๊ฐœ๋ฐœํ•˜๊ฒŒ ๋œ๋‹ค.

SAX(Simple API for XML) ๋ฐฉ์‹

  • SAX ๋ฐฉ์‹์€ XML ๋ฌธ์„œ๋ฅผ ํ•˜๋‚˜์˜ ๊ธด ๋ฌธ์ž์—ด๋กœ ๊ฐ„์ฃผํ•œ๋‹ค.

  • XML๋ฌธ์„œ๋ฅผ ์•ž์—์„œ ๋ถ€ํ„ฐ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฝ์–ด๊ฐ€๋ฉด์„œ ๋…ธ๋“œ๊ฐ€ ์—ด๋ฆฌ๊ณ  ๋‹ซํžˆ๋Š” ๊ณผ์ •์—์„œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

  • ๊ฐ๊ฐ์˜ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒ๋  ๋•Œ๋งˆ๋‹ค ์ˆ˜ํ–‰ํ•˜๊ณ ์ž ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๊ธฐ์ˆ ์„ ์ด์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•œ๋‹ค.

  • XML๋ฌธ์„œ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ์ „๋ถ€ ๋กœ๋”ฉํ•˜๊ณ  ํŒŒ์‹ฑํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ์ ๊ณ  ๋‹จ์ˆœํžˆ ์ฝ๊ธฐ๋งŒ ํ• ๋•Œ ์†๋„๊ฐ€ ๋น ๋ฅด๋‹ค.

  • ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ๋ฅผ ํ•ธ๋“ค๋งํ•˜์—ฌ ๋ณ€์ˆ˜์— ์ €์žฅํ•˜๊ณ  ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ณต์žกํ•˜๊ณ  ๋…ธ๋“œ ์ˆ˜์ •์ด์–ด๋ ต๋‹ค.

  • XML ์˜ค๋ธŒ์ ํŠธ์— Random Access๋ฅผ ํ•˜์ง€ ๋ชปํ•ด, ์ง€๋‚œ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ฐธ์กฐํ•  ๊ฒฝ์šฐ ๋‹ค์‹œ ์ฒ˜์Œ๋ถ€ํ„ฐ ์ฝ์–ด์•ผํ•œ๋‹ค.

StAX(Streaming API for XML)

  • StAX๋Š” push ์™€ pull ๋ฐฉ์‹์„ ๋™์‹œ์— ์ œ๊ณตํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œํ•œ ํ˜•ํƒœ

  • XML ๋ฌธ์„œ๋ฅผ ํŒŒ์‹ฑํ• ๋•Œ ํ•˜๋‚˜์˜ Fragment๋กœ ๊ตฌ๋ถ„

    • ์ •ํ•ด์ง„ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ฝ์„๋•Œ๋Š” DOM ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋ฉฐ, Fragement๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์€ SAX์˜ Push ๋ฐฉ์‹์„ ์‚ฌ์šฉ

    • ์ฆ‰, ๊ฐ ์„ธ์…˜์„ ๋…๋ฆฝ์ ์œผ๋กœ ํŒŒ์‹ฑํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณต

์Šคํ”„๋ง ๋ฐฐ์น˜์—์„œ๋Š” StAX ํŒŒ์„œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

StaxEventItemReader

  • org.springframework.batch.item.xml.StaxEventItemReader

<customers>
    <customer>
        <firstName>Laura</firstName>
        <middleInitial>O</middleInitial>
        <lastName>Minella</lastName>
        <address>2039 Wall Street</address>
        <city>Omaha</city>
        <state>IL</state>
        <zipCode>35446</zipCode>
        <transactions>
            <transaction>
                <accountNumber>829433</accountNumber>
                <transactionDate>2010-10-14 05:49:58</transactionDate>
                <amount>26.08</amount>
            </transaction>
        </transactions>
    </customer>
  ...
</customers>

์œ„ ์˜ˆ์ œ ํŒŒ์ผ์„ ํŒŒ์‹ฑํ•˜๋Š” Reader๋ฅผ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒƒ์ด๋‹ค.

@Bean
    @StepScope
    public StaxEventItemReader<Customer> staxCustomerFileReader(@Value("#{jobParameters['customFile']}")Resource resource) {
        return new StaxEventItemReaderBuilder<Customer>()
                .name("staxCustomerFileReader")
                .resource(resource)
                .addFragmentRootElements("customer") // ํ”„๋ ˆ๊ทธ๋จผํŠธ ๋ฃจํŠธ ์—˜๋ฆฌ๋จผํŠธ
                .unmarshaller(customerMarshaller()) // XML์„ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜ JAXB ์‚ฌ์šฉ
                .build();
    }
  • .addFragmentRootElements() : StaxEventItemReader๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด XML ํ”„๋ž˜ํฌ๋จผํŠธ ๋ฃจํŠธ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ง€์ • XML๋‚ด์—์„œ Item์œผ๋กœ ์ทจ๊ธ‰ํ•  fragment์˜ root ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์‹๋ณ„ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ

  • .unmarshaller() : org.springframework.oxm.Unmarshaller ๊ตฌํ˜„์ฒด๋ฅผ ์ „๋‹ฌ ๋ฐ›์œผ๋ฉฐ, XML์„ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜

์ด๋ฒˆ ์˜ˆ์ œ์—์„œ๋Š” org.springframework.oxm.jaxb.Jaxb2Marshaller๋ฅผ ์‚ฌ์šฉํ–ˆ์œผ๋ฉฐ, Jaxb2Marshaller๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์˜์กด์„ฑ ์ถ”๊ฐ€๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

build.gradle

dependencies {
    implementation 'org.springframework:spring-oxm'
    implementation 'javax.xml.bind:jaxb-api:2.3.1'
    implementation 'javax.activation:activation:1.1'
    implementation 'com.sun.xml.bind:jaxb-core:2.3.0.1'
    implementation 'com.sun.xml.bind:jaxb-impl:2.3.1'
}

JAXB ์˜์กด์„ฑ๊ณผ Spring OXM ๋ชจ๋“ˆ๋กœ JAXB๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์Šคํ”„๋ง ์ปดํฌ๋„ŒํŠธ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

XML์„ ํŒŒ์‹ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋ ค๋ฉด ๋„๋งค์ธ ๊ฐ์ฒด์— JAXB ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•ด์ค„ ๊ฒƒ์ด๋‹ค.

@NoArgsConstructor
@Getter
@Setter
@ToString
@XmlRootElement
public class Customer {

    private Long id;
    private String firstName;
    private String middleInitial;
    private String lastName;
    private String addressNumber;
    private String street;
    private String city;
    private String state;
    private String zipCode;

    private String address; // customAddressMapper

    private List<Transaction> transactions;

    @XmlElementWrapper(name = "transactions")
    @XmlElement(name = "transaction")
    public void setTransactions(List<Transaction> transactions) {
        this.transactions = transactions;
    }
}
@Getter
@Setter
@ToString
@XmlType(name = "transaction")
public class Transaction {
    private String accountNumber;
    private Date transactionDate;
    private Double amount;

    @Setter(value = AccessLevel.NONE)
    private DateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy");

}

๋„๋ฉ”์ธ ๊ฐ์ฒด ์„ค์ •์ด ๋๋‚ฌ์œผ๋ฉด, ๊ฐ ๋ธ”๋ก์„ ํŒŒ์‹ฑํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•  Unmarshaller๋ฅผ ๊ตฌํ˜„ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

		@Bean
    public Jaxb2Marshaller customerMarshaller() {
        Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
        jaxb2Marshaller.setClassesToBeBound(Customer.class, Transaction.class); // ๋„๋ฉ”์ธ ๊ฐ์ฒด

        return jaxb2Marshaller;
    }

JSON

JsonItemReader

  • org.springframework.batch.item.json.JsonItemReader

  • ์ฒญํฌ๋ฅผ ์ฝ์–ด ๊ฐ์ฒด๋กœ ํŒŒ์‹ฑํ•œ๋‹ค.

  • ์‹ค์ œ ํŒŒ์‹ฑ ์ž‘์—…์€ JsonObjectReader ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„์ฒด์— ์œ„์ž„ํ•œ๋‹ค.

JsonObjectReader

์‹ค์ œ๋กœ JSON ๊ฐ์ฒด๋ฅผ ํŒŒ์‹ฑํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. ์Šคํ”„๋ง ๋ฐฐ์น˜๋Š” 2๊ฐœ์˜ JsonObjectReader๋ฅผ ์ œ๊ณตํ•ด์ค€๋‹ค.

  • Jackson

  • Gson

[
  {
    "firstName": "Laura",
    "middleInitial": "O",
    "lastName": "Minella",
    "address": "2039 Wall Street",
    "city": "Omaha",
    "state": "IL",
    "zipCode": "35446",
    "transactions": [
      {
        "accountNumber": 829433,
        "transactionDate": "2010-10-14 05:49:58",
        "amount": 26.08
      }
    ]
  },
  ...
]

์œ„ ๊ตฌ์กฐ๋กœ๋˜์–ด์žˆ๋Š” JSON์„ ํŒŒ์‹ฑํ•ด ๋ณผ๊ฒƒ์ด๋‹ค.

		@Bean
    @StepScope
    public JsonItemReader<Customer> jsonFileReader(@Value("#{jobParameters['customFile']}") Resource resource) {

        // Jackson์ด JSON์„ ์ฝ๊ณ  ์“ฐ๋Š”๋ฐ ์‚ฌ์šฉํ•˜๋Š” ์ฃผ์š” ํด๋ž˜์Šค
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));

        JacksonJsonObjectReader<Customer> jsonObjectReader = new JacksonJsonObjectReader<>(Customer.class); // ๋ฐ˜ํ™˜ํ•  ํด๋ž˜์Šค ์„ค์ •
        jsonObjectReader.setMapper(objectMapper); // ObjectMapper

        return new JsonItemReaderBuilder<Customer>()
                .name("jsonFileReader")
                .jsonObjectReader(jsonObjectReader) // ํŒŒ์‹ฑ์— ์‚ฌ์šฉ
                .resource(resource)
                .build();
    }
  • ObjectMapper๋Š” Jackson์ด JSON์„ ์ฝ๊ณ  ์“ฐ๋Š”๋ฐ ์‚ฌ์šฉํ•˜๋Š” ์ฃผ์š” ํด๋ž˜์Šค๋กœ ์ปค์Šคํ…€ ๋ฐ์ดํ„ฐ ํฌ๋งท๋“ค์„ ์„ค์ •ํ•˜๋ฉด๋œ๋‹ค.

  • JacksonJsonObjectReader ์ƒ์„ฑ์‹œ ๋ฐ˜ํ™˜ํ•  ํด๋ž˜์Šค๋ฅผ ์„ค์ •ํ•˜๊ณ , ์ปค์Šคํ…€ํ•œ ObjectMapper๋ฅผ ์„ค์ •ํ•ด์ฃผ๋ฉด๋œ๋‹ค.

  • JsonItemReaderBuilder๋Š” ํŒŒ์‹ฑ์— ์‚ฌ์šฉํ•  JsonObjectReader๋ฅผ ์„ค์ •ํ•ด์ฃผ๋ฉด๋œ๋‹ค.

Database Reader

cursorvspaging

Cursor๋Š” ํ‘œ์ค€ java.sql.ResultSet์œผ๋กœ ๊ตฌํ˜„๋˜๋ฉฐ, ResultSet์ด open๋˜๋ฉด next() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ฐฐ์น˜ ๋ ˆ์ฝ”๋“œ๋ฅผ ๊ฐ€์ ธ์™€ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

์ฆ‰, Cursor ๋ฐฉ์‹์€ DB์™€ ์ปค๋„ฅ์…˜์„ ๋งบ์€ ํ›„, Cursor๋ฅผ ํ•œ์นธ์”ฉ ์˜ฎ๊ธฐ๋ฉด์„œ ์ง€์†์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

(CursorItemReader๋Š” streaming์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌ)

Cursor๋Š” ํ•˜๋‚˜์˜ Connection์œผ๋กœ Batch๊ฐ€ ๋๋‚ ๋•Œ๊ฐ€์ง€ ์‚ฌ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ์— Batch๊ฐ€ ๋๋‚˜๊ธฐ์ „์— DB์™€ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ Connection์ด ๋Š์–ด์งˆ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, DB์™€ SocketTimeout์„ ์ถฉ๋ถ„ํžˆ ํฐ ๊ฐ’์œผ๋กœ ์„ค์ •ํ•ด์•ผํ•œ๋‹ค. (๋„คํŠธ์›Œํฌ ์˜ค๋ฒ„ํ—ค๋“œ ์ถ”๊ฐ€) ์ถ”๊ฐ€๋กœ ResultSet์€ ์Šค๋ ˆ๋“œ ์•ˆ์ „์ด ๋ณด์žฅ๋˜์ง€ ์•Š์•„ ๋‹ค์ค‘ ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.

  • JdbcCursorItemReader

  • HibernateCursorItemReader

  • StoredProcedureItemReader

Paging ๋ฐฉ์‹์€ ํ•œ๋ฒˆ์— ์ง€์ •ํ•œ PageSize๋งŒํผ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

SpringBatch์—์„œ offset๊ณผ limit์„ PageSize์— ๋งž๊ฒŒ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์ค€๋‹ค. ๋‹ค๋งŒ ๊ฐ ์ฟผ๋ฆฌ๋Š” ๊ฐœ๋ณ„์ ์œผ๋กœ ์‹คํ–‰๋˜๋ฏ€๋กœ, ํŽ˜์ด์ง•์‹œ ๊ฒฐ๊ณผ๋ฅผ ์ •๋ ฌ(order by)ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค.

Batch ์ˆ˜ํ–‰์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ๊ฒฝ์šฐ์—๋Š” PagingItemReader๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. Paging์˜ ๊ฒฝ์šฐ ํ•œ ํŽ˜์ด์ง€๋ฅผ ์ฝ์„๋•Œ๋งˆ๋‹ค Connection์„ ๋งบ๊ณ  ๋Š๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ฌด๋ฆฌ ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ผ๋„ ํƒ€์ž„์•„์›ƒ๊ณผ ๋ถ€ํ•˜ ์—†์ด ์ˆ˜ํ–‰๋  ์ˆ˜ ์žˆ๋‹ค.

JDBC

JdbcCursorItemReader

/**
 * --job.name=jdbcCursorItemReaderJob city=Chicago
 */
@RequiredArgsConstructor
@Configuration
public class JdbcCursorCustomerJob {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final DataSource dataSource;

    @Bean
    public Job jdbcCursorItemReaderJob(){
        return jobBuilderFactory.get("jdbcCursorItemReaderJob")
                .start(jdbcCursorItemReaderStep())
                .build();
    }

    @Bean
    public Step jdbcCursorItemReaderStep(){
        return stepBuilderFactory.get("jdbcCursorItemReaderStep")
                .<Customer, Customer>chunk(10)
                .reader(customerJdbcCursorItemReader())
                .writer(customerJdbcCursorItemWriter())
                .build();
    }

    @Bean
    public JdbcCursorItemReader<Customer> customerJdbcCursorItemReader() {
        return new JdbcCursorItemReaderBuilder<Customer>()
                .name("customerJdbcCursorItemReader")
                .dataSource(dataSource)
                .sql("select * from customer where city = ?")
                .rowMapper(new BeanPropertyRowMapper<>(Customer.class))
                .preparedStatementSetter(citySetter(null))      // ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •
                .build();
    }

    /**
     * ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ SQL๋ฌธ์— ๋งคํ•‘
     * ArgumentPreparedStatementSetter๋Š” ๊ฐ์ฒด ๋ฐฐ์—ด์— ๋‹ด๊ธด ์ˆœ์„œ๋Œ€๋กœ ?์˜ ์œ„์น˜์— ๊ฐ’์œผ๋กœ ์„ค์ •
     * @param city
     * @return
     */
    @Bean
    @StepScope
    public ArgumentPreparedStatementSetter citySetter(@Value("#{jobParameters['city']}") String city) {
        return new ArgumentPreparedStatementSetter(new Object[]{city});
    }

    @Bean
    public ItemWriter customerJdbcCursorItemWriter() {
        return (items) -> items.forEach(System.out::println);
    }
}
  • <T, T> chunk(int chunkSize) : ์ฒซ๋ฒˆ์งธ T๋Š” Reader์—์„œ ๋ฐ˜ํ™˜ํ•  ํƒ€์ž…, ๋‘๋ฒˆ์งธ T๋Š” Writer์— ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜์–ด์˜ฌ ํƒ€์ž…์ด๋‹ค.

  • fetchSize : DB์—์„œ ํ•œ๋ฒˆ์— ๊ฐ€์ ธ์˜ฌ ๋ฐ์ดํ„ฐ ์–‘์„ ๋‚˜ํƒ€๋‚ธ๋‹ค. Paging์€ ์‹ค์ œ ์ฟผ๋ฆฌ๋ฅผ limit, offset์œผ๋กœ ๋ถ„ํ•  ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐ˜๋ฉด, Cursor๋Š” ๋ถ„ํ•  ์ฒ˜๋ฆฌ์—†์ด ์‹คํ–‰๋˜๋‚˜ ๋‚ด๋ถ€์ ์œผ๋กœ ๊ฐ€์ ธ์˜จ๋Š” ๋ฐ์ดํ„ฐ๋Š” FetchSize๋งŒํผ ๊ฐ€์ ธ์™€ read()๋ฅผ ํ†ตํ•ด์„œ ํ•˜๋‚˜์”ฉ ๊ฐ€์ ธ์˜จ๋‹ค.

  • dataSource : DB์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•  DataSource๊ฐ์ฒด

  • rowMapper : ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ์ธ์Šคํ„ด์Šค๋กœ ๋งคํ•‘ํ•˜๊ธฐ ์œ„ํ•œ ๋งคํผ

    • BeanPropertyRowMapper ๋ฅผ ์‚ฌ์šฉํ•ด ๋„๋ฉ”์ธ ๊ฐ์ฒด์™€ ๋งคํ•‘ํ•ด์ค€๋‹ค.

  • sql : Reader์—์„œ ์‚ฌ์šฉํ•  ์ฟผ๋ฆฌ๋ฌธ

  • preparedStatementSetter: SQL๋ฌธ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •

  • name : Reader์˜ ์ด๋ฆ„, ExecutionContext์— ์ €์žฅ๋˜์–ด์งˆ ์ด๋ฆ„

JdbcPagingItemReader

/**
 * --job.name=jdbcPagingItemReaderJob city=Chicago
 */
@RequiredArgsConstructor
@Configuration
public class JdbcPagingCustomerJob {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final DataSource dataSource;

    @Bean
    public Job jdbcPagingItemReaderJob(){
        return jobBuilderFactory.get("jdbcPagingItemReaderJob")
                .start(jdbcPagingItemReaderStep())
                .build();
    }

    @Bean
    public Step jdbcPagingItemReaderStep(){
        return stepBuilderFactory.get("jdbcPagingItemReaderStep")
                .<Customer, Customer>chunk(10)
                .reader(customerJdbcPagingItemReader(null, null))
                .writer(customerJdbcPagingItemWriter())
                .build();
    }

    @Bean
    @StepScope
    public JdbcPagingItemReader<Customer> customerJdbcPagingItemReader(
            PagingQueryProvider pagingQueryProvider, @Value("#{jobParameters['city']}") String city) {

        Map<String, Object> params = new HashMap<>(1);
        params.put("city", city);


        return new JdbcPagingItemReaderBuilder<Customer>()
                .name("customerJdbcPagingItemReader")   // Reader์˜ ์ด๋ฆ„, ExecutionContext์— ์ €์žฅ๋˜์–ด์งˆ ์ด๋ฆ„
                .dataSource(dataSource)                 // DB์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•  DataSource๊ฐ์ฒด
                .queryProvider(pagingQueryProvider)     // PagingQueryProvider
                .parameterValues(params)                // SQL ๋ฌธ์— ์ฃผ์ž…ํ•ด์•ผํ•  ํŒŒ๋ผ๋ฏธํ„ฐ
                .pageSize(10)                           // ๊ฐ ํŽ˜์ด์ง€ ํฌ
                .rowMapper(new BeanPropertyRowMapper<>(Customer.class)) // ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ์ธ์Šคํ„ด์Šค๋กœ ๋งคํ•‘ํ•˜๊ธฐ ์œ„ํ•œ ๋งคํผ
                .build();
    }

    @Bean
    public ItemWriter customerJdbcPagingItemWriter() {
        return (items) -> items.forEach(System.out::println);
    }


    @Bean
    public SqlPagingQueryProviderFactoryBean pagingQueryProvider(){
        SqlPagingQueryProviderFactoryBean queryProvider = new SqlPagingQueryProviderFactoryBean();
        queryProvider.setDataSource(dataSource); // ์ œ๊ณต๋œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ํƒ€์ž…์„ ๊ฒฐ์ •(setDatabaseType ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํƒ€์ž… ์„ค์ •๋„ ๊ฐ€๋Šฅ)
        queryProvider.setSelectClause("*");
        queryProvider.setFromClause("from customer");
        queryProvider.setWhereClause("where city = :city");

        Map<String, Order> sortKeys = new HashMap<>();
        sortKeys.put("lastName", Order.ASCENDING);

        queryProvider.setSortKeys(sortKeys);

        return queryProvider;
    }
}

PagingItemReader๋Š” PagingQueryProvider๋ฅผ ํ†ตํ•ด ์ฟผ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ์ด๋ ‡๊ฒŒ ์ƒ์„ฑํ•˜๋Š” ์ด์œ ๋Š” ๊ฐ DB์—๋Š” Paging์„ ์ง€์›ํ•˜๋Š” ์ž์ฒด์ ์ธ ์ „๋žต์ด ์žˆ์œผ๋ฉฐ, Spring์€ ๊ฐ DB์˜ Paging ์ „๋žต์— ๋งž์ถฐ ๊ตฌํ˜„๋˜์–ด์•ผ๋งŒ ํ•œ๋‹ค.

public SqlPagingQueryProviderFactoryBean() {
        this.providers.put(DatabaseType.DB2, new Db2PagingQueryProvider());
        this.providers.put(DatabaseType.DB2VSE, new Db2PagingQueryProvider());
        this.providers.put(DatabaseType.DB2ZOS, new Db2PagingQueryProvider());
        this.providers.put(DatabaseType.DB2AS400, new Db2PagingQueryProvider());
        this.providers.put(DatabaseType.DERBY, new DerbyPagingQueryProvider());
        this.providers.put(DatabaseType.HSQL, new HsqlPagingQueryProvider());
        this.providers.put(DatabaseType.H2, new H2PagingQueryProvider());
        this.providers.put(DatabaseType.MYSQL, new MySqlPagingQueryProvider());
        this.providers.put(DatabaseType.ORACLE, new OraclePagingQueryProvider());
        this.providers.put(DatabaseType.POSTGRES, new PostgresPagingQueryProvider());
        this.providers.put(DatabaseType.SQLITE, new SqlitePagingQueryProvider());
        this.providers.put(DatabaseType.SQLSERVER, new SqlServerPagingQueryProvider());
        this.providers.put(DatabaseType.SYBASE, new SybasePagingQueryProvider());
    }

Spring Batch์—์„œ๋Š” SqlPagingQueryProviderFactoryBean์„ ํ†ตํ•ด DataSource ์„ค์ • ๊ฐ’์„ ๋ณด๊ณ , ์œ„ Provider์ค‘ ํ•˜๋‚˜๋ฅผ ์ž๋™ ์„ ํƒํ•˜๋„๋ก ํ•œ๋‹ค.

SpringBatch์—์„œ offset๊ณผ limit์„ PageSize์— ๋งž๊ฒŒ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์ค€๋‹ค. ๋‹ค๋งŒ ๊ฐ ์ฟผ๋ฆฌ๋Š” ๊ฐœ๋ณ„์ ์œผ๋กœ ์‹คํ–‰๋˜๋ฏ€๋กœ, ๋™์ผํ•œ ๋ ˆ์ฝ”๋“œ ์ •๋ ฌ ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•˜๋ ค๋ฉด ํŽ˜์ด์ง•์‹œ ๊ฒฐ๊ณผ๋ฅผ ์ •๋ ฌ(order by)ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค.

๋˜ํ•œ, ์ด ์ •๋ ฌํ‚ค๊ฐ€ ResultSet ๋‚ด์—์„œ ์ค‘๋ณต๋˜์ง€ ์•Š์•„์•ผํ•œ๋‹ค.

SELECT * FROM customer WHERE city = :city ORDER BY lastName ASC LIMIT 10

์‹คํ–‰๋œ ์ฟผ๋ฆฌ ๋กœ๊ทธ๋ฅผ ๋ณด๋ฉด Paging Size์ธ LIMIT 10์ด ๋“ค์–ด๊ฐ„ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

Hibernate

  • ์ž๋ฐ” ORM ๊ธฐ์ˆ ๋กœ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ์ฒด ์ง€ํ–ฅ ๋ชจ๋ธ์„ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ๋งคํ•‘ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณต

  • XML or Annotation์„ ์‚ฌ์šฉํ•ด ๊ฐ์ฒด๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”๋กœ ๋งคํ•‘

  • ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์งˆ์˜ํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ œ๊ณต

Hibernate ์„ธ์…˜ ๊ตฌํ˜„์ฒด์— ๋”ฐ๋ผ์„œ ๋‹ค๋ฅด๊ฒŒ ์ž‘๋™ํ•œ๋‹ค.

  • ๋ณ„๋„ ์„ค์ •์—†์ด Hibernate๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ผ๋ฐ˜์ ์ธ stateful ์„ธ์…˜ ๊ตฌํ˜„์ฒด๋ฅผ ์‚ฌ์šฉ

    • ์˜ˆ๋ฅผ ๋“ค์–ด ๋ฐฑ๋งŒ๊ฑด์˜ ์•„์ดํ…œ์„ ์ฝ๊ณ  ์ฒ˜๋ฆฌํ•œ๋‹ค๋ฉด Hibernate ์„ธ์…˜์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์กฐํšŒํ•  ๋•Œ ์•„์ดํ…œ์„ ์บ์‹œ์— ์Œ“์œผ๋ฉฐ OutOfMemoryException์ด ๋ฐœ์ƒ

  • Persistence๋กœ ์‚ฌ์šฉํ•˜๋ฉด, ์ง์ ‘ JDBC๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋ณด๋‹ค ๋” ํฐ ๋ถ€ํ•˜๋ฅผ ์œ ๋ฐœ

    • ๋ ˆ์ฝ”๋“œ ๋ฐฑ๋งŒ ๊ฑด์„ ์ฒ˜๋ฆฌํ•  ๋•Œ๋Š” ํ•œ๊ฑด๋‹น ms ๋‹จ์œ„์˜ ์ฐจ์ด๋„ ๊ฑฐ๋Œ€ํ•œ ์ฐจ์ด๊ฐ€ ๋œ๋‹ค.

  • ์Šคํ”„๋ง ๋ฐฐ์น˜๋Š” ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋„๋ก HibernateCursorItemReader, HibernatePagingItemReader๋ฅผ ๊ฐœ๋ฐœ

    • ์ปค๋ฐ‹์‹œ ์„ธ์…˜์„ flushํ•˜๋ฉฐ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์— ๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” ์ถ”๊ฐ€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์ค€๋‹ค.

build.gradle ์˜์กด์„ฑ ์ถ”๊ฐ€

compileOnly 'org.springframework.boot:spring-boot-starter-data-jpa'

Domain ๊ฐ์ฒด ์ˆ˜์ •

@Entity
@Table(name = "customer")
public class Customer {

    @Id
    private Long id; // pk
    private String firstName;
    private String middleInitial;
    private String lastName;
    private String address;
    private String city;
    private String state;
    private String zipCode;
}
์–ด๋…ธํ…Œ์ด์…˜
์„ค๋ช…

@Entity

๋งคํ•‘ํ•  ๊ฐ์ฒด๊ฐ€ Entity์ž„์„ ๋‚˜ํƒ€๋ƒ„

@Table

Entity๊ฐ€ ๋งคํ•‘๋˜๋Š” ํ…Œ์ด๋ธ” ์ง€์ •

@Id

PK๊ฐ’ ์ง€์ •

TransactionManager ์ปค์Šคํ…€

ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ์„ธ์…˜๊ณผ Datasource๋ฅผ ํ•ฉ์นœ TransactionManager๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

@Component
public class HibernateBatchConfigurer extends DefaultBatchConfigurer {

    private DataSource dataSource;
    private SessionFactory sessionFactory;
    private PlatformTransactionManager transactionManager;

    /**
     * Datasource connection๊ณผ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ์„ธ์…˜ ์„ค์ •
     * @param dataSource
     * @param entityManagerFactory
     */
    public HibernateBatchConfigurer(DataSource dataSource,
                                    EntityManagerFactory entityManagerFactory) {
        super(dataSource);
        this.dataSource = dataSource;
        this.sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);

        // ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ํŠธ๋žœ์žญ์…˜ ์„ค์ •
        this.transactionManager = new HibernateTransactionManager(this.sessionFactory);
    }

    @Override
    public PlatformTransactionManager getTransactionManager() {
        return this.transactionManager;
    }
}

BatchConfigurer์˜ ์ปค์Šคํ…€ ๊ตฌํ˜„์ฒด๋ฅผ ์‚ฌ์šฉํ•ด HibernateTransactionManager๋ฅผ ๊ตฌ์„ฑํ–ˆ๋‹ค.

HibernateCursorItemReader

/**
 * --job.name=hibernateCursorItemReaderJob city=Chicago
 */
@RequiredArgsConstructor
@Configuration
public class HibernateCursorCustomerJob {

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

    @Bean
    public Job hibernateCursorItemReaderJob(){
        return jobBuilderFactory.get("hibernateCursorItemReaderJob")
                .start(hibernateCursorItemReaderStep())
                .build();
    }

    @Bean
    public Step hibernateCursorItemReaderStep(){
        return stepBuilderFactory.get("hibernateCursorItemReaderStep")
                .<Customer, Customer>chunk(10)
                .reader(customerHibernateCursorItemReader(null))
                .writer(customerHibernateCursorItemWriter())
                .build();
    }

    @Bean
    @StepScope
    public HibernateCursorItemReader<Customer> customerHibernateCursorItemReader(@Value("#{jobParameters['city']}") String city) {
        return new HibernateCursorItemReaderBuilder<Customer>()
                .name("customerHibernateCursorItemReader") // Reader์˜ ์ด๋ฆ„, ExecutionContext์— ์ €์žฅ๋˜์–ด์งˆ ์ด๋ฆ„
                .sessionFactory(entityManagerFactory.unwrap(SessionFactory.class))
                .queryString("from Customer where city = :city")        // HQL ์ฟผ๋ฆฌ
                .parameterValues(Collections.singletonMap("city", city)) // SQL ๋ฌธ์— ์ฃผ์ž…ํ•ด์•ผํ•  ํŒŒ๋ผ๋ฏธํ„ฐ
                .build();
    }

    @Bean
    public ItemWriter customerHibernateCursorItemWriter() {
        return (items) -> items.forEach(System.out::println);
    }
}

ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ 4๊ฐ€์ง€๊ฐ€ ์กด์žฌํ•œ๋‹ค.

์˜ต์…˜
ํƒ€์ž…
์„ค๋ช…
์˜ˆ์ œ

queryName

String

ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ๊ตฌ์„ฑ์— ํฌํ•จ๋œ ๋„ค์ž„๋“œ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ์ฟผ๋ฆฌ ์ฐธ์กฐ

https://www.baeldung.com/hibernate-named-query

queryString

String

์Šคํ”„๋ง ๊ตฌ์„ฑ์— ์ถ”๊ฐ€ํ•˜๋Š” HQL ์ฟผ๋ฆฌ

.queryString("from Customer where city = :city")

queryProvider

HibernateQueryProvider

ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ์ฟผ๋ฆฌ(HQL)๋ฅผ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์œผ๋กœ ๋นŒ๋“œ

nativeQuery

String

๋„ค์ดํ‹ฐ๋ธŒ SQL ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•œ ๋’ค ๊ฒฐ๊ณผ๋ฅผ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ๋กœ ๋งคํ•‘ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ

https://data-make.tistory.com/616

HibernatePagingItemReader

/**
 * --job.name=hibernatePagingItemReaderJob city=Chicago
 */
@RequiredArgsConstructor
@Configuration
public class HibernatePagingCustomerJob {

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

    @Bean
    public Job hibernatePagingItemReaderJob() {
        return jobBuilderFactory.get("hibernatePagingItemReaderJob")
                .start(hibernatePagingItemReaderStep())
                .build();
    }

    @Bean
    public Step hibernatePagingItemReaderStep() {
        return stepBuilderFactory.get("hibernatePagingItemReaderStep")
                .<Customer, Customer>chunk(10)
                .reader(customerHibernatePagingItemReader(null))
                .writer(customerHibernatePagingItemWriter())
                .build();
    }

    @Bean
    @StepScope
    public HibernatePagingItemReader<Customer> customerHibernatePagingItemReader(@Value("#{jobParameters['city']}") String city) {
        return new HibernatePagingItemReaderBuilder<Customer>()
                .name("customerHibernatePagingItemReader") // Reader์˜ ์ด๋ฆ„, ExecutionContext์— ์ €์žฅ๋˜์–ด์งˆ ์ด๋ฆ„
                .sessionFactory(entityManagerFactory.unwrap(SessionFactory.class))
                .queryString("from Customer where city = :city")        // HQL ์ฟผ๋ฆฌ
                .parameterValues(Collections.singletonMap("city", city)) // SQL ๋ฌธ์— ์ฃผ์ž…ํ•ด์•ผํ•  ํŒŒ๋ผ๋ฏธํ„ฐ
                .pageSize(10)       // Cursor์™€ ์œ ์ผํ•œ ์ฐจ์ด์ ! pageSize ์„ค์ •
                .build();
    }

    @Bean
    public ItemWriter customerHibernatePagingItemWriter() {
        return (items) -> items.forEach(System.out::println);
    }
}

Cursor ๋ฐฉ๋ฒ•๊ณผ ์œ ์ผํ•˜๊ฒŒ ๋‹ค๋ฅธ ์ ์€ .pageSize()๋กœ ์‚ฌ์šฉํ•  ํŽ˜์ด์ง€ ํฌ๊ธฐ๋ฅผ ์ง€์ •ํ•ด์•ผํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

JPA

JPA(Java Persisstence API)๋Š” ORM ์˜์—ญ์—์„œ ํ‘œ์ค€ํ™”๋œ ์ ‘๊ทผ๋ฒ•์„ ์ œ๊ณตํ•œ๋‹ค. Hibernate๊ฐ€ ์ดˆ๊ธฐ JPA์— ์˜๊ฐ์„ ์คฌ์œผ๋ฉฐ, ํ˜„์žฌ๋Š” Hibernate๊ฐ€ JPA ๋ช…์„ธ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋‹ค.

build.gradle ์˜์กด์„ฑ ์ถ”๊ฐ€

compileOnly 'org.springframework.boot:spring-boot-starter-data-jpa'

spring-boot-starter-data-jpa๋Š” JPA๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ํ•„์š”ํ•œ ๋ชจ๋“  ํ•„์ˆ˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํฌํ•จ๋ผ์žˆ๋‹ค.

JpaCursorItemReader

Spring Batch 4.3์ด ๋ฆด๋ฆฌ์ฆˆ ๋˜๋ฉด์„œ JpaCursorItemReader ๊ฐ€ ๋„์ž…๋˜์—ˆ๋‹ค. ์ด์ „๋ฒ„์ „๊นŒ์ง€๋Š” ์ œ๊ณตํ•˜์ง€ ์•Š์•˜๋‹ค.

/**
 * --job.name=jpaCursorItemReaderJob city=Chicago
 */
@RequiredArgsConstructor
@Configuration
public class JpaCursorCustomerJob {

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

    @Bean
    public Job jpaCursorItemReaderJob(){
        return jobBuilderFactory.get("jpaCursorItemReaderJob")
                .start(jpaCursorItemReaderStep())
                .build();
    }

    @Bean
    public Step jpaCursorItemReaderStep(){
        return stepBuilderFactory.get("jpaCursorItemReaderStep")
                .<Customer, Customer>chunk(10)
                .reader(customerJpaCursorItemReader(null))
                .writer(customerJpaCursorItemWriter())
                .build();
    }

    @Bean
    @StepScope
    public JpaCursorItemReader<Customer> customerJpaCursorItemReader(@Value("#{jobParameters['city']}") String city) {
        return new JpaCursorItemReaderBuilder<Customer>()
                .name("customerJpaCursorItemReader")
                .entityManagerFactory(entityManagerFactory)
                .queryString("select c from Customer c where c.city = :city")
                .parameterValues(Collections.singletonMap("city", city))
                .build();
    }

    @Bean
    public ItemWriter customerJpaCursorItemWriter() {
        return (items) -> items.forEach(System.out::println);
    }
}

JpaPagingItemReader

/**
 * --job.name=jpaPagingItemReaderJob city=Chicago
 */
@RequiredArgsConstructor
@Configuration
public class JpaPagingCustomerJob {

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

    @Bean
    public Job jpaPagingItemReaderJob(){
        return jobBuilderFactory.get("jpaPagingItemReaderJob")
                .start(jpaPagingItemReaderStep())
                .build();
    }

    @Bean
    public Step jpaPagingItemReaderStep(){
        return stepBuilderFactory.get("jpaPagingItemReaderStep")
                .<Customer, Customer>chunk(10)
                .reader(customerJpaPagingItemReader(null))
                .writer(customerJpaPagingItemWriter())
                .build();
    }

    @Bean
    @StepScope
    public JpaPagingItemReader<Customer> customerJpaPagingItemReader(@Value("#{jobParameters['city']}") String city) {
        return new JpaPagingItemReaderBuilder<Customer>()
                .name("customerJpaPagingItemReader")
                .entityManagerFactory(entityManagerFactory)
                .queryString("select c from Customer c where c.city = :city")
                .parameterValues(Collections.singletonMap("city", city))
                .pageSize(10)
                .build();
    }

    @Bean
    public ItemWriter customerJpaPagingItemWriter() {
        return (items) -> items.forEach(System.out::println);
    }
}

.entityManagerFactory๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฒƒ ์ด์™ธ์— Jdbc์™€ ํฌ๊ฒŒ ๋‹ค๋ฅธ ์ ์€ ์—†์œผ๋ฉฐ, Cursor์™€ ๋‹ค๋ฅธ์ ์€ pageSize()๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

JPA์—์„œ๋Š” .queryProvider()๋กœ Query ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ด ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

MyBatisPagingItemReader

<!--mybatis-config.xml-->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="defaultStatementTimeout" value="25"/>
    </settings>
</configuration>
package spring.batch.practice.config;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Slf4j
@Configuration
public class MysqlMybatisConfig {

    @Value("${mybatis.mapper-locations}")
    private String mapperLocations;

    @Bean(name = "mybatisDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.hikari")
    public DataSource dataSource(){
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "mybatisSqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("mybatisDataSource") DataSource dataSource, ApplicationContext applicationContext) throws Exception{
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource("classpath:mybatis/mybatis-config.xml"));
        Resource[] resources = new PathMatchingResourcePatternResolver().getResources(mapperLocations);
        System.out.println(resources[0].getURL());
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));

        return sqlSessionFactoryBean.getObject();

    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("mybatisSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="spring.batch.practice.dao.PayMapper">

    <select id="selectPayList" parameterType="hashmap" resultType="spring.batch.practice.domain.Pay">
	<![CDATA[
        SELECT ID, AMOUNT, TX_NAME, TX_DATE_TIME
        FROM PAY
        WHERE AMOUNT <= #{amount}
        ]]>
	</select>

</mapper>
@Slf4j
@RequiredArgsConstructor
@Configuration
public class MybatisPagingItemReaderJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Autowired
    @Qualifier("mybatisSqlSessionFactory")
    private SqlSessionFactory sqlSessionFactory;


    private static final int CHUNK_SIZE = 10;

    @Bean
    public Job mybatisPagingItemReaderJob() throws Exception {
        return jobBuilderFactory.get("mybatisPagingItemReaderJob")
                .start(mybatisPagingItemReaderStep())
                .build();
    }
    @Bean
    public Step mybatisPagingItemReaderStep() throws Exception {
        return stepBuilderFactory.get("mybatisPagingItemReaderStep")
                .<Pay, Pay>chunk(CHUNK_SIZE)
                .reader(mybatisPagingItemReader())
                .writer(mybatisPagingItemWriter())
                .build();
    }
    @Bean
    public MyBatisPagingItemReader<Pay> mybatisPagingItemReader() throws Exception {
        Map<String, Object> params = new HashMap<>();
        params.put("amount", 2000);

        return new MyBatisPagingItemReaderBuilder<Pay>()
                .pageSize(CHUNK_SIZE)
                .sqlSessionFactory(sqlSessionFactory)
                .queryId("spring.batch.practice.dao.PayMapper.selectPayList")
                .parameterValues(params)
                .build();
    }
    @Bean
    public ItemWriter<Pay> mybatisPagingItemWriter() {
        return list -> {
            for (Pay pay : list) {
                log.info("Current Pay={}", pay);
            }
        };
    }
}

Spring Data Repository

Spring Data๋Š” ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ํŠน์ • ์ธํ„ฐํŽ˜์ด์Šค ์ค‘ ํ•˜๋‚˜๋ฅผ ์ƒ์†ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉ์ž๊ฐ€ ์ •์˜ํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ๊ฐ€ ํ•ด๋‹น ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ตฌํ˜„์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

์Šคํ”„๋ง ๋ฐฐ์น˜๋Š” ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ์˜ PagingAndSotringRepository๋ฅผ ํ™œ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ์™€ ํ˜ธํ™˜์„ฑ์ด ์ข‹๋‹ค.

RepositoryItemReader

RepositoryItemReader๋Š” JdbcPagingItemReader ๋‚˜ HibernatePagingItemReader๋ฅผ ์‚ฌ์šฉํ• ๋•Œ์™€ ๋™์ผํ•˜๊ฒŒ PagingAndSotringRepository๋ฅผ ์‚ฌ์šฉํ•ด์„œ Paging ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.

RepositoryItemReader๋Š” ์–ด๋–ค ์ €์žฅ์†Œ๊ฑด ์ƒ๊ด€์—†์ด ํ•ด๋‹น ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ์— ์งˆ์˜๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์—์„œ ItemReader์™€ ์ฐจ์ด๊ฐ€ ์žˆ๋‹ค.

public interface CustomerRepository extends JpaRepository<Customer, Long> {
    Page<Customer> findByCity(String city, Pageable pageRequest);
}

PagingAndSotringRepository๋ฅผ ์ƒ์†ํ•˜๋Š” Repository๋ฅผ ์ƒ์„ฑํ•ด city ์กฐ๊ฑด์œผ๋กœ ์กฐํšŒํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•˜์˜€๋‹ค.

		@Bean
    @StepScope
    public RepositoryItemReader<Customer> customerRepositoryItemReader(@Value("#{jobParameters['city']}") String city) {
        return new RepositoryItemReaderBuilder<Customer>()
                .name("customerRepositoryItemReader")
                .arguments(Collections.singletonList(city)) // pageable ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ œ์™ธํ•œ arguments
                .methodName("findByCity")                   // ํ˜ธ์ถœํ•  ๋ฉ”์„œ๋“œ๋ช…
                .repository(customerRepository)             // Repository ๊ตฌํ˜„์ฒด
                .sorts(Collections.singletonMap("lastName", Sort.Direction.ASC))
                .build();
    }

์ฃผ์˜ ์‚ฌํ•ญ

  • JpaRepository๋ฅผ ListItemReader, QueueItemReader์— ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ๋œ๋‹ค.

    • ์ด๋ ‡๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒฝ์šฐ Spring Batch์˜ ์žฅ์ ์ธ Paging & Cursor ๊ตฌํ˜„์ด ์—†์–ด ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.

    • JpaRepository๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•˜๋Š” ๊ฒฝ์šฐ RepositoryItemReader๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•œ๋‹ค.

  • Hibernate, JPA๋“ฑ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๊ฐ€ ํ•„์š”ํ•œ Reader ์‚ฌ์šฉ์‹œ fetch size์™€ chunk size๋Š” ๋™์ผํ•œ ๊ฐ’์„ ์œ ์ง€ํ•ด์•ผ ํ•œ๋‹ค.

ItemReaderAdapter

Adapter๋Š” ๋‹ค๋ฅธ ์—˜๋ฆฌ๋ฉ˜ํŠธ์™€ ๋ž˜ํ•‘ํ•˜์—ฌ ์Šคํ”„๋ง ๋ฐฐ์น˜๊ฐ€ ํ•ด๋‹น ์—˜๋ฆฌ๋จผํŠธ์™€ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•œ๋‹ค.

  • org.springframework.batch.item.adapter.ItemReaderAdapter

public class ItemReaderAdapter<T> extends AbstractMethodInvokingDelegator<T> implements ItemReader<T> {

	/**
	 * @return return value of the target method.
	 */
	@Nullable
	@Override
	public T read() throws Exception {
		return invokeDelegateMethod();
	}

}

ํ˜ธ์ถœ ๋Œ€์ƒ ์„œ๋น„์Šค์˜ ์ฐธ์กฐ์™€ ํ˜ธ์ถœํ•  ๋ฉ”์„œ๋“œ์˜ ์ด๋ฆ„์„ ์˜์กด์„ฑ์œผ๋กœ ๋ฐ›๋Š”๋‹ค.

  • ItemReaderAdapter๊ฐ€ ๋งค๋ฒˆ ํ˜ธ์ถœํ•  ๋•Œ๋งˆ๋‹ค ๋ฐ˜ํ™˜๋˜๋Š” ๊ฐ์ฒด๋Š” ItemReader๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ์ฒด์ด๋‹ค.

  • ์ž…๋ ฅ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ•˜๋ฉด ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ๋Š” ๋ฐ˜๋“œ์‹œ null์„ ๋ฐ˜ํ™˜ํ•ด์•ผํ•œ๋‹ค. ์Šคํ”„๋ง ๋ฐฐ์น˜์—๊ฒŒ ํ•ด๋‹น step์˜ ์ž…๋ ฅ์„ ๋ชจ๋‘ ์™„๋ฃŒํ–ˆ์Œ์„ ์•Œ๋ฆฌ๋Š” ๊ฒƒ์ด๋‹ค.

@Component
public class CustomerService {
    private List<Customer> customers;
    private int curIndex;

    private String [] firstNames = {"Michael", "Warren", "Ann", "Terrence",
            "Erica", "Laura", "Steve", "Larry"};
    private String middleInitial = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private String [] lastNames = {"Gates", "Darrow", "Donnelly", "Jobs",
            "Buffett", "Ellison", "Obama"};
    private String [] streets = {"4th Street", "Wall Street", "Fifth Avenue",
            "Mt. Lee Drive", "Jeopardy Lane",
            "Infinite Loop Drive", "Farnam Street",
            "Isabella Ave", "S. Greenwood Ave"};
    private String [] cities = {"Chicago", "New York", "Hollywood", "Aurora",
            "Omaha", "Atherton"};
    private String [] states = {"IL", "NY", "CA", "NE"};

    private Random generator = new Random();

    public CustomerService() {
        curIndex = 0;

        customers = new ArrayList<>();

        for(int i = 0; i < 100; i++) {
            customers.add(buildCustomer());
        }
    }

    private Customer buildCustomer() {
        Customer customer = new Customer();

        customer.setId((long) generator.nextInt(Integer.MAX_VALUE));
        customer.setFirstName(
                firstNames[generator.nextInt(firstNames.length - 1)]);
        customer.setMiddleInitial(
                String.valueOf(middleInitial.charAt(
                        generator.nextInt(middleInitial.length() - 1))));
        customer.setLastName(
                lastNames[generator.nextInt(lastNames.length - 1)]);
        customer.setAddress(generator.nextInt(9999) + " " +
                streets[generator.nextInt(streets.length - 1)]);
        customer.setCity(cities[generator.nextInt(cities.length - 1)]);
        customer.setState(states[generator.nextInt(states.length - 1)]);
        customer.setZipCode(String.valueOf(generator.nextInt(99999)));

        return customer;
    }

    public Customer getCustomer() {
        Customer cust = null;

        if(curIndex < customers.size()) {
            cust = customers.get(curIndex);
            curIndex++;
        }

        return cust;
    }
}

Customer ๊ฐ์ฒด์˜ ๋ชฉ๋ก์„ ๋ฌด์ž‘์œ„๋กœ ์ƒ์„ฑํ•˜๋Š” ์„œ๋น„์Šค์ด๋‹ค.

    @Bean
    public ItemReaderAdapter<Customer> customerServiceItemReader() {
        ItemReaderAdapter<Customer> adapter = new ItemReaderAdapter<>();

        adapter.setTargetObject(customerService);
        adapter.setTargetMethod("getCustomer");

        return adapter;
    }

ItemReaderAdapter์— ๊ธฐ์กด ์„œ๋น„์Šค ์˜ค๋ธŒ์ ํŠธ์™€ ๋ฉ”์„œ๋“œ๋ช…์„ ์ „๋‹ฌํ•˜๋ฉด๋œ๋‹ค.

์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ

์ž…๋ ฅ์—์„œ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ฝ๋Š” ์ค‘์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์€ ์—ฌ๋Ÿฌ๊ฐ€์ง€์ด๋‹ค.

  1. ์˜ˆ์™ธ๋ฅผ ๋˜์ณ ์ฒ˜๋ฆฌ๋ฅผ ๋ฉˆ์ถ”๊ธฐ

  2. ํŠน์ • ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ ๋ ˆ์ฝ”๋“œ ๊ฑด๋„ˆ๋„๊ธฐ(skip)

Skip

  1. ์–ด๋–ค ์กฐ๊ฑด์—์„œ ๋ ˆ์ฝ”๋“œ๋ฅผ skipํ• ์ง€(์–ด๋–ค ์˜ˆ์™ธ๋ฅผ ๋ฌด์‹œํ•  ๊ฒƒ์ธ์ง€)

  2. ์–ผ๋งˆ๋‚˜ ๋งŽ์€ ๋ ˆ์ฝ”๋“œ๋ฅผ skip ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ• ๊ฒƒ์ธ์ง€

๋ ˆ์ฝ”๋“œ๋ฅผ skipํ• ์ง€ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•  ๋•Œ ์œ„ ๋‘๊ฐ€์ง€ ์š”์†Œ๋ฅผ ๊ณ ๋ คํ•ด์•ผํ•œ๋‹ค.

Skip์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์šฐ์„  faultToLerant()๋ผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ด์•ผํ•œ๋‹ค.

๋ฉ”์„œ๋“œ
์„ค๋ช…

skipLimit()

skip ํ—ˆ์šฉ ํšŒ์ˆ˜. ํ—ˆ์šฉ ํšŒ์ˆ˜๋ฅผ ๋„˜์–ด๊ฐ€๋ฉด job์€ ์‹คํŒจํ•œ๋‹ค. skip()๊ณผ ๋ฐ˜๋“œ์‹œ ๊ฐ™์ด ์จ์•ผ ํ•œ๋‹ค

skip()

ํ•ด๋‹น exception์ด ๋ฐœ์ƒํ–ˆ์„๋•Œ skip

noSkip()

ํ•ด๋‹น exception์ด ๋ฐœ์ƒํ•˜๋ฉด skip์„ ํ•˜์ง€ ์•Š๊ณ  ์˜ค๋ฅ˜๋ฅผ ๋‚ด๊ฒ ๋‹ค๋Š” ๊ฒƒ

skipPolicy()

์šฉ์ž ์ •์˜๋กœ skip์— ๋Œ€ํ•œ policy๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ ์šฉํ•˜๊ณ  ์‹ถ์„๋•Œ ์‚ฌ์šฉ

    @Bean
    public Step skipRecordCopyFileStep() {
        return this.stepBuilderFactory.get("skipRecordCopyFileStep")
                .<Customer, Customer>chunk(10)
                .reader(null)
                .writer(null)
                .faultTolerant()
                .skip(Exception.class)
                .noSkip(ParseException.class)
                .skipLimit(10)
                .build();
    }

SkipPolicy

public interface SkipPolicy {

	boolean shouldSkip(Throwable t, int skipCount) throws SkipLimitExceededException;
}

SkipPolicy ๊ตฌํ˜„์ฒด๋Š” skipํ•  ์˜ˆ์™ธ์™€ ํ—ˆ์šฉ ํšŸ์ˆ˜๋ฅผ ํŒ๋ณ„ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, boolean ๊ฐ’์œผ๋กœ ๋‚ด๋ถ€ ๋กœ์ง์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

public class FileVerificationSkipper implements SkipPolicy {
    @Override
    public boolean shouldSkip(Throwable t, int skipCount) throws SkipLimitExceededException {
        
        if (t instanceof FileNotFoundException) {
            return false;
        } else if (t instanceof ParseException && skipCount < 10) {
            return true;
        } else {
            return false;
        }
    }
}

์˜ค๋ฅ˜ ๋กœ๊ทธ ๋‚จ๊ธฐ๊ธฐ

ItemListener๋ฅผ ์‚ฌ์šฉํ•ด ์ž˜๋ชป๋œ ๋ ˆ์ฝ”๋“œ๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค๋ฃฐ ๊ฒƒ์ด๋‹ค.

public interface ItemReadListener<T> extends StepListener {

	void beforeRead();
	
	void afterRead(T item);
	
	void onReadError(Exception ex);
}

์ž˜๋ชป๋œ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ฝ์—ˆ์„ ๋•Œ ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๊ธฐ ์œ„ํ•ด์„œ onReadError ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฐ”๋ผ์ด๋“œํ•ด ItemListenerSupport๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—๋Ÿฌ๋ฅผ ๊ธฐ๋กํ•œ๋‹ค.

๋‹จ์ˆœํžˆ ํŒŒ์ผ์„ ์ฝ์–ด์˜ฌ ๋•Œ๋Š” ์œ„์™€ ๊ฐ™์ด ์ฒ˜๋ฆฌํ•˜๋ฉด ๋˜์ง€๋งŒ, DB๋ฅผ ์ด์šฉํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ์—๋Š” ์‹ค์ œ DB์ž…์ถœ๋ ฅ์„ ์Šคํ”„๋ง ์ž์ฒด๋‚˜ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ์™€ ๊ฐ™์€ ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ, ์Šคํ”„๋ง ๋ฐฐ์น˜์—์„œ ์ฒ˜๋ฆฌํ•  ์˜ˆ์™ธ๊ฐ€ ๋งŽ์ง€ ์•Š๋‹ค.

onReadError ํŒŒ์ผ ์˜ค๋ฅ˜ ๋กœ๊ทธ ๋‚จ๊ธฐ๊ธฐ

@Slf4j
public class CustomerItemListener {

    @OnReadError
    public void onReadError(Exception e) {
        if (e instanceof FlatFileParseException) {
            FlatFileParseException ffpe = (FlatFileParseException) e;

            StringBuilder sb = new StringBuilder();
            sb.append("์˜ค๋ฅ˜ ๋ฐœ์ƒ ๋ผ์ธ : ");
            sb.append(ffpe.getLineNumber());
            sb.append("์ž…๋ ฅ๊ฐ’ : ");
            sb.append(ffpe.getInput());

            log.error(sb.toString(), ffpe);
        } else {
            log.error("์˜ค๋ฅ˜ ๋ฐœ์ƒ", e);
        }
    }
}
    @Bean
		public CustomerItemListener customerItemListener(){
      	return new CustomerItemListener();
    }
    @Bean
    public Step copyFileStep() {
        return this.stepBuilderFactory.get("skipRecordCopyFileStep")
                .<Customer, Customer>chunk(10)
                .reader(null)
                .writer(null)
                .faultTolerant()
                .skip(Exception.class)
                .skipLimit(10)
          			.listener(customerItemListener())
                .build();
    }

Last updated

Was this helpful?