[스프링 배치] 두가지 포맷의 파일을 각 포맷(접두어)에 따라 처리하기 (PatternMatchingCompositeLineMapper , LineTokenizer, FieldSetMapper

반응형
728x90
반응형

읽어올 파일

1. customerMultiFormat.csv

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

 

 

DTO

1. Transaction.java

import lombok.Getter;
import lombok.Setter;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.xml.bind.annotation.XmlType;

@XmlType(name = "transaction")
@Getter
@Setter
public class Transaction {

    private String accountNumber;
    private Date transactionDate;
    private Double amount;

    private DateFormat formatter =
            new SimpleDateFormat("MM/dd/yyyy");

    public String getDateString() {
        return this.formatter.format(this.transactionDate);
    }

    @Override
    public String toString() {
        return "Transaction{" +
                "accountNumber='" + accountNumber + '\'' +
                ", transactionDate=" + transactionDate +
                ", amount=" + amount +
                '}';
    }
}

 

2. Customer.java

package com.seohae.batch.batch.fileBatch1.entity;

import com.seohae.batch.batch.fileBatch2.entity.Transaction;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.List;

@Entity
@Table(name = "customer")
@Getter
@Setter
public class Customer {
    @Id
    private Long id;

    @Column(name = "firstName")
    private String firstName;

    @Column(name = "middleInitial")
    private String middleInitial;

    @Column(name = "lastName")
    private String lastName;

    private String addressNumber;
    private String street;
    private String address;
    private String city;
    private String state;
    private String zipCode;

    private List<Transaction> transactions;

    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;
	}
}

 

 

Reader

1. customerItemReader 메서드

...	
    @Bean
    @StepScope
    public FlatFileItemReader<Customer> customerItemReader(
            @Value("#{jobParameters['customerFile']}")Resource inputFile) {
        return new FlatFileItemReaderBuilder<Customer>()
                .name("customerItemReader")
                .lineMapper(lineTokenizer())
                .resource(inputFile)
                .build();
    }
...

 

.lineMapper(lineTokenizer()) 호출 부분에 대해 자세히 알아보자.

 

 

 

lineTokenizer

1. lineTokenizer

...
    @Bean
    public PatternMatchingCompositeLineMapper lineTokenizer() {
        /* LineTokenizer */
        Map<String, LineTokenizer> lineTokenizers = new HashMap<>(2);

        lineTokenizers.put("CUST*", customerLineTokenizer()); /* 고객 패턴일 경우 */
        lineTokenizers.put("TRANS*", transactionLineTokenizer()); /* 거래내역 패턴일 경우 */

        /* fieldSetMapper */
        Map<String, FieldSetMapper> fieldSetMappers = new HashMap<>(2);

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

        fieldSetMappers.put("CUST*", customerFieldSetMapper); /* 고객 패턴일 경우 */
        fieldSetMappers.put("TRANS*", new TransactionFieldSetMapper()); /* 거래내역 패턴일 경우 (타입 변환) */

        /* lineMappers */
        PatternMatchingCompositeLineMapper lineMappers = new PatternMatchingCompositeLineMapper();
        lineMappers.setTokenizers(lineTokenizers);
        lineMappers.setFieldSetMappers(fieldSetMappers);

        return lineMappers;
    }

 

  • 1-(1). LineTokenizer
Map<String, LineTokenizer> lineTokenizers = new HashMap<>(2);

lineTokenizers.put("CUST*", customerLineTokenizer()); /* 고객 패턴일 경우 */
lineTokenizers.put("TRANS*", transactionLineTokenizer()); /* 거래내역 패턴일 경우 */

 

PatternMatchingCompositeLineMapper로 구성하여 리턴한다. Map 타입의 lineTokenizers 는 아래 DelimitedLineTokenizer 인스턴스 2개를 put 한다. 각 DelimitedLineTokenizer 은 레코드 포맷에 맞는 필드 구성을 한다. 각 레코드의 시작 부분 (CUST*, TRANS*)을 처리하는데에 각 LineTokenizer에 접두어 추가 필드가 포함되어있다. 

 

이렇게 설정되어, PatternMatchingCompositeLineMapper가 파일에서 각 레코드를 찾아서 미리 정의된 패턴과 비교한다. 레코드가 CUST* 로 시작하면 customerLineTokenizer 에 전달하여 파싱한다. 레코드를 FieldSet 으로 파싱한 뒤에는 파싱된 FieldSet 을 FieldSetMapper 로 전달한다. 매핑 작업은 계속해서 아래의 BeanWrapperFieldSetMapper 관련 코드로 알아보자.

 

 

  • customerLineTokenizer()
...
    @Bean
    public DelimitedLineTokenizer customerLineTokenizer() {
        DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();

        lineTokenizer.setNames("firstName", "middleInitial", "lastName", "address", "city", "state", "zipCode");
        lineTokenizer.setIncludedFields(1, 2, 3, 4, 5, 6, 7);

        return lineTokenizer;
    }

 

  • transactionLineTokenizer()
...
    @Bean
    public DelimitedLineTokenizer transactionLineTokenizer() {
        DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
        lineTokenizer.setNames("prefix", "accountNumber", "transactionDate", "amount");

        return lineTokenizer;
    }

 

 

  • 1-(2). FieldSetMapper
Map<String, FieldSetMapper> fieldSetMappers = new HashMap<>(2);

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

fieldSetMappers.put("CUST*", customerFieldSetMapper); /* 고객 패턴일 경우 */
fieldSetMappers.put("TRANS*", new TransactionFieldSetMapper()); /* 거래내역 패턴일 경우 (타입 변환) */

 

BeanWrapperFieldSetMapper 는 FieldSet 을 도메인 객체의 필드에 매핑한다. Customer 도메인 객체에는 prefix 필드가 없었으므로, customerLineTokenizer 가 prefix 필드를 건너뛰게 해야한다. targetType 을 Customer.class 로 설정하여 모든 필드를 포함시킨다.

 

레코드가 TRANS*로 시작하면, 해당 레코드를 transactionLineTokenizer 로 전달한다. FieldSet으로 파싱된 데이터를 커스텀한 transactionFieldSetMapper 로 전달한다.  아래 transactionFieldSetMapper.java 의 코드를 파악해보자.

 

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

 

  • TransactionFieldSetMapper.java
import com.seohae.batch.batch.fileBatch2.entity.Transaction;
import org.springframework.batch.item.file.mapping.FieldSetMapper;
import org.springframework.batch.item.file.transform.FieldSet;
import org.springframework.validation.BindException;

public class TransactionFieldSetMapper implements FieldSetMapper<Transaction> {

    /**
     * 데이터 타입을 읽어와, 타입 변환 커스텀 메서드
     * @param fieldSet
     * @return
     * @throws BindException
     */
    @Override
    public Transaction mapFieldSet(FieldSet fieldSet) throws BindException {
        Transaction trans = new Transaction();
        trans.setAccountNumber(fieldSet.readString("accountNumber"));
        trans.setAmount(fieldSet.readDouble("amount"));
        trans.setTransactionDate(fieldSet.readDate("transactionDate", "yyyy-MM-dd HH:mm:ss"));

        return trans;
    }
}

 

FieldSetMapper는 일반적이지 않은 타입의 필드를 변환할때 필요하다. 위에서 봐온 BeanWrapperFieldSetMapper 는 특수한 타입의 필드를 변환할 수 없다. Transaction 도메인 객체에는 String 타입의 필드가 포함되어있고, 해당 필드는 변환에 문제가 없지만 Date 타입과 Double 타입인 객체 변환은 FieldSetMapper를 사용하여 타입을 변환시켜야한다. 위 코드를 보면 각 타입변환을 해주고있는데, 이를 위해 FieldSetMapper 커스텀을 생성한 것이다.

 

 

  • 1-(3). PatternMatchingCompositeLineMapper
 PatternMatchingCompositeLineMapper lineMappers = new PatternMatchingCompositeLineMapper();
 lineMappers.setTokenizers(lineTokenizers);
 lineMappers.setFieldSetMappers(fieldSetMappers);

 

 

 

 

 

교재 '스프링배치 완벽가이드' 책 예제 따라 Job 생성해보기

 

 

 

반응형

Designed by JB FACTORY