읽어올 파일
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 생성해보기
'Coding > Spring Batch' 카테고리의 다른 글
SpringBatch 에서 JobInstance, JobExecution 의 관계 (0) | 2022.01.14 |
---|---|
[스프링 배치] Spring Batch + mysql 설정하기 (0) | 2021.10.09 |
[스프링 배치] resources/ 경로의 파일 읽어와 여러 방법으로 필드 매핑하기 (FieldSetMapper, LineTokenizer) (0) | 2021.10.01 |
[스프링 배치] 잡의 재시작 방지/재시작 횟수제한/재실행 설정 방법 (0) | 2021.09.25 |
[스프링 배치] 오류 던지기 (throw new Exception) (2) | 2021.09.25 |