본문 바로가기
Framework/Spring-Batch

(8-1) Spring Batch FlatFileItemReader

by 조훙 2022. 11. 16.

FlatFileItemReader

  • 일반적인 CSV, TSV 또는 고정형 등 파일 처리용 ItemReader 구현체
  • 구현 스키마가 파일에 없는 경우에 사용 할 수 있다
  • Resource(파일) 과 LineMapper(DefaultLineMapper) 구현체로 구성

구현 관계

  • Resource
    • FileSystemResource : 절대 경로 또는 파일 리소스를 찾는 방식
    • ClassPathResource: ClassLoader를 통해서 ClassPath를 통해 리소스를 찾는 방식
  • LineMapper
    • 파일의 row를 Object로 변환해서 FlatFileItemReader로 Return
    • 파일을 읽은 문자열을 토큰화 하여 Object에 Mapping 하는 과정
    • FieldSet : row를 Fields로 구분하여 만든 Token으로 자른 결과
    • LineTokenizer: 입력 받은 row를 FieldSet으로 변환해서 제공
    • FieldSetMapper: FieldSet Object를 POJO 형태로 매핑

실제 처리 절차

FixedLengthTokenizer

  • 고정형 파일을 처리 하기 위한 Tokenizer

고정형 Sample file

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
Petra      ZLara      8401Et St.              Georgia         GA70323
Cherokee   TLara      8516Mauris St.          Seattle         WA28720
Athena     YBurt      4951Mollis Rd.          Newark          DE41034
Kaitlin    MMacias    5715Velit St.           Chandler        AZ86176
Leroy      XCherry    7810Vulputate St.       Seattle         WA37703
Connor     WMontoya   4122Mauris Av.          College         AK99743
Byron      XMedina    7875At Road             Rock Springs    WY37733
Penelope   YSandoval  2643Fringilla Av.       College         AK99557
Rashad     VOchoa     6587Lacus Street        Flint           MI96640
Jordan     UOneil     170 Mattis Ave          Bellevue        WA44941
Caesar     ODaugherty 7483Libero Ave          Frankfort       KY56493
Wynne      DRoth      8086Erat Street         Owensboro       KY50476
Robin      IRoberson  8014Pellentesque Street Casper          WY84633
Adrienne   CCarpenter 8141Aliquam Avenue      Tucson          AZ85057

고정형 Sample Code

/**
* Customer.java
*/
@Getter
@Setter
public class Customer {
    private String firstName;
    private String middleInitial;
    private String lastName;
    private String addressNumber;
    private String street;
    private String address;
    private String city;
    private String state;
    private String zipCode;

    public Customer() {
    }

    public Customer(String firstName, String middleName, String lastName, String addressNumber, String street, String city, String state, String zipCode) {
        this.firstName = firstName;
        this.middleInitial = middleName;
        this.lastName = lastName;
        this.addressNumber = addressNumber;
        this.street = street;
        this.city = city;
        this.state = state;
        this.zipCode = zipCode;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "firstName='" + firstName + '\'' +
                ", middleInitial='" + middleInitial + '\'' +
                ", lastName='" + lastName + '\'' +
                ", address='" + address + '\'' +
                ", addressNumber='" + addressNumber + '\'' +
                ", street='" + street + '\'' +
                ", city='" + city + '\'' +
                ", state='" + state + '\'' +
                ", zipCode='" + zipCode + '\'' +
                '}';
    }
}


/**
* FixedWidthJobConfiguration.java
*/
@Configuration
public class FixedWidthJobConfiguration {
    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Bean
    @StepScope
    public FlatFileItemReader<Customer> fixedWidthItemReader(
            @Value("#{jobParameters['customerFile']}") Resource inputFile) {

        return new FlatFileItemReaderBuilder<Customer>()
                .name("fixedWidthItemReader")
                .resource(inputFile)
                .fixedLength()
                .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"})
                .targetType(Customer.class)
                .build();
    }

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

    @Bean
    public Step fixedWidthStep() {
        return this.stepBuilderFactory.get("fixedWidthStep")
                .<Customer, Customer>chunk(10)
                .reader(fixedWidthItemReader(null))
                .writer(fixedWidthItemWriter())
                .build();
    }

    @Bean
    public Job fixedWidthJob() {
        return this.jobBuilderFactory.get("fixedWidthJob")
                .start(fixedWidthStep())
                .build();
    }
}

DelimitedLineTokenizer

  • CSV, TSV 등 가변형 데이터를 처리 하기 위한 Tokenizer

가변형 Sample data

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

가변형 Sample Code

/**
* FlatFileItemReader.java
*/
@Bean
@StepScope
public FlatFileItemReader<Customer> customerFlatFileItemReader(
        @Value("#{jobParameters['customerFile']}") Resource inputFile) {

    logger.info("Activated flat file reader: delimited");

    return new FlatFileItemReaderBuilder<Customer>()
            .name("customerItemReader")
            .resource(inputFile)
            .delimited()
            .delimiter(",")                               // default ","
            .names("firstName",
                   "middleInitial",
                   "lastName",
                   "addressNumber",
                   "street",
                   "city",
                   "state",
                   "zipCode")
            .targetType(Customer.class)
            .build();
}

만약 사용자가 address + street를 합쳐서 address 로 변경 하려면 FieldSetMapper를 Customize 해야 한다.

/**
 * {@link BeanWrapperFieldSetMapper} 대신 {@link CustomerFieldMapper}를 이용한다.
 */
@Bean
@StepScope
public FlatFileItemReader<Customer> customerFlatFileItemReader(
        @Value("#{jobParameters['customerFile']}") Resource inputFile) {
    logger.info("Activated flat file reader: custom field mapper");

    return new FlatFileItemReaderBuilder<Customer>()
            // ItemStream 인터페이스는 애플리케이션 내 각 스텝의 ExecutionContext에 추가되는 특정 키의
            // 접두문자로 사용될 이름이 필요
            .name("customerItemReader")
            .delimited()
            .names("firstName",
                   "middleInitial",
                   "lastName",
                   "addressNumber",
                   "street",
                   "city",
                   "state",
                   "zipCode")
            .fieldSetMapper(new CustomerFieldMapper())
            .resource(inputFile)
            .build();
}

public static class CustomerFieldMapper implements FieldSetMapper<Customer> {

    @Override
    public Customer mapFieldSet(FieldSet fieldSet) throws BindException {
        return Customer.builder()
                       .firstName(fieldSet.readString("firstName"))
                       .middleInitial(fieldSet.readString("middleInitial"))
                       .lastName(fieldSet.readString("lastName"))
                       .address(fieldSet.readString("addressNumber") + " " + fieldSet.readString("street"))
                       .city(fieldSet.readString("city"))
                       .state(fieldSet.readString("state"))
                       .zipCode(fieldSet.readString("zipCode"))
                       .build();
    }
}

Custom Record Parsing 하기 위한 LineTokenizer 사용 예시

@Bean
@StepScope
public FlatFileItemReader<Customer> customerFlatFileItemReader(
        @Value("#{jobParameters['customerFile']}") Resource inputFile) {
		// @Value를 사용하기 위해서는 `@StepScope`를 선언 해야 한다.
    // Spring Bean이 생성되는 시점이 아닌 `@StepScope`가 수행 되는 시점에 지연 시키는것(Late Binding)
    // Bean이 생성되는 시점에는 inputFile 값을 초기화 할 수 없으므로 에러가 발생함!!!
    return new FlatFileItemReaderBuilder<Customer>()
            .name("customerItemReader")
            .lineTokenizer(new CustomerFileLineTokenizer())
            .targetType(Customer.class)
            .resource(inputFile)
            .build();
}

public static class CustomerFileLineTokenizer implements LineTokenizer {

    private final String delimiter = ",";
    private final String[] names = {
            "firstName",
            "middleInitial",
            "lastName",
            "address",
            "city",
            "state",
            "zipCode"
    };
    private final FieldSetFactory fieldSetFactory = new DefaultFieldSetFactory();

    @Override
    public FieldSet tokenize(String record) {
        final String[] fields = record.split(delimiter);
        final List<String> parsedFields = new ArrayList<>();

        for (int i = 0; i < fields.length; i++) {
            if (i == 4) {
                parsedFields.set(i - 1, parsedFields.get(i - 1) + " " + fields[i]);
                continue;
            }
            parsedFields.add(fields[i]);
        }

        return fieldSetFactory.create(parsedFields.toArray(new String[0]), names);
    }
}

Reference

'Framework > Spring-Batch' 카테고리의 다른 글

(10) Spring Batch ItemWriter  (0) 2022.11.16
(9) Spring Batch ItemProcessor  (0) 2022.11.16
(8) Spring Batch ItemReader  (0) 2022.11.16
(7) Spring Batch Chunk  (0) 2022.11.16
(6) Spring Batch Step  (0) 2022.11.16