Spring Batch 입문 27편. Flat File (CSV · 고정 길이) 의 두 타입, 파싱 단계의 3총사 — LineMapper · LineTokenizer · FieldSetMapper 의 협력 구조, RecordSeparatorPolicy · linesToSkip · comments · encoding 같은 옵션 그림, 표준 ItemReader/Writer 와의 연결까지 정리한 학습 노트. Part 6 시작.
이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 27편이에요. 26편 까지 ItemReader · ItemWriter 의 일반 인터페이스 를 끝냈다면, 이번 27편부터 Part 6 — File · DB Reader/Writer 심화 로 들어갑니다. 첫 주제는 Flat File 의 전체 그림.
Flat File 이 여전히 중요한 이유
One of the most common mechanisms for interchanging bulk data has always been the flat file. — 공식 reference
대량 데이터 교환에 JSON·XML 도 쓰지만 현실 운영 에서 가장 흔한 건 여전히 flat file 이에요. 은행·증권의 결제·정산 데이터, 대형 시스템 간 daily batch 인터페이스, 레거시 시스템 통합, 데이터 웨어하우스 적재 (ETL — 추출·변환·적재 파이프라인), 외부 파트너 데이터 교환이 다 flat file 로 오갑니다.
→ Spring Batch 의 Flat File 지원 = batch 환경의 기본기.
Flat File 의 두 타입
1. Delimited — 구분자 분리
1,Alice,alice@example.com,2026-05-17
2,Bob,bob@example.com,2026-05-17
3,"Carol, Jr.",carol@example.com,2026-05-17
구분자 (,·|·\t) 로 필드를 나누고 길이는 가변. 특수 문자는 quoting 으로 처리해요 (예: "Carol, Jr."). 가장 흔한 형식.
2. Fixed Length — 고정 길이
0001Alice alice@example.com 20260517
0002Bob bob@example.com 20260517
0003Carol carol@example.com 20260517
각 필드의 시작 위치 + 길이 가 고정이라 구분자가 없고 position 기반 으로 잘라요. 레거시·금융 시스템에서 자주 보이고, 길이는 공백 padding 으로 맞춥니다.
공통 함정 — Schema 가 외부
XML 과 달리, flat file 을 읽는 측은 파일 구조를 사전 합의 해야 한다. — 공식 reference
XML 은 XSD (XML Schema Definition) 로, JSON 은 JSON Schema 로 검증해요. Flat file 은 schema 가 파일 안에 없음. 별도 합의 문서 가 필요합니다.
→ Reader 코드 안에 컬럼 이름·순서·길이 명시 필수. 변경 시 코드·합의 문서 동기화.
파싱 3총사 — LineMapper · LineTokenizer · FieldSetMapper
Flat file Reader 의 내부 흐름:
1. File → 1줄씩 line (String) 읽음 [FlatFileItemReader]
2. line (String) → FieldSet (필드 분리) [LineTokenizer]
3. FieldSet → 도메인 객체 (POJO 매핑) [FieldSetMapper]
이 셋을 묶는 것 = LineMapper
각자 책임 분리 라 컴포넌트별로 교체할 수 있어요. POJO 는 Plain Old Java Object, 평범한 자바 객체를 가리킵니다.
LineMapper — 통합 인터페이스
public interface LineMapper<T> {
T mapLine(String line, int lineNumber) throws Exception;
}
String line (한 줄) + int lineNumber → 도메인 객체 변환.
표준 구현 = DefaultLineMapper — LineTokenizer + FieldSetMapper 묶음.
LineTokenizer — 필드 분리
public interface LineTokenizer {
FieldSet tokenize(String line);
}
한 줄 → FieldSet (필드 배열) 변환.
표준 구현 3가지:
| 구현 | 용도 |
|---|---|
DelimitedLineTokenizer |
CSV·TSV 등 구분자 기반 |
FixedLengthTokenizer |
고정 길이 |
PatternMatchingCompositeLineTokenizer |
줄별로 다른 tokenizer (예: header·body 다른 형식) |
FieldSetMapper — POJO 매핑
public interface FieldSetMapper<T> {
T mapFieldSet(FieldSet fieldSet) throws BindException;
}
FieldSet → 도메인 객체.
표준 구현:
| 구현 | 용도 |
|---|---|
BeanWrapperFieldSetMapper |
property 이름 매칭 (setter) |
RecordFieldSetMapper (Spring Batch 5+) |
Java Record |
PassThroughFieldSetMapper |
FieldSet 자체 반환 (변환 X) |
| Custom | 복잡 변환 로직 |
FieldSet 의 의미 — 다음 글에서 깊게
FieldSet = flat file 의 line 을 추상화 한 표준 객체.
- index 또는 name 으로 접근 —
fieldSet.readString(0)/fieldSet.readString("name") - type 변환 —
readLong·readDouble·readDate - null·empty 처리
다음 글 (28편) 에서 깊게.
FlatFileItemReader 의 전체 옵션 그림
@Bean
public FlatFileItemReader<Customer> customerReader() {
return new FlatFileItemReaderBuilder<Customer>()
.name("customerReader")
.resource(new FileSystemResource("customers.csv"))
.encoding("UTF-8") // 인코딩
.linesToSkip(1) // header 건너뛰기
.comments("#", "//") // 주석 line
.recordSeparatorPolicy(new DefaultRecordSeparatorPolicy())
.delimited() // delimited 모드
.delimiter(",")
.quoteCharacter('"')
.names("id", "name", "email", "createdAt")
.targetType(Customer.class)
.build();
}
핵심 옵션:
name — ExecutionContext key prefix
ItemStreamSupport 의 name. 재시작 안전성 의 key unique 보장.
resource — 입력 파일
Resource 추상화 — FileSystem·Classpath·URL·InputStream 모두 OK.
encoding — 인코딩
기본 = JVM default (UTF-8). 한국어 파일 = UTF-8 또는 EUC-KR 명시.
linesToSkip — 건너뛸 line 수
CSV header 가 1줄이면 linesToSkip(1).
comments — 주석 prefix
해당 prefix 로 시작하는 line = skip. 예: comments("#") → # 시작 line 무시.
recordSeparatorPolicy — multi-line record 처리
기본 = 1 line = 1 record. 단 quoted multi-line CSV 같은 경우 DefaultRecordSeparatorPolicy 가 quote 안 newline 합침.
delimited() / fixedLength() / lineTokenizer()
빌더 분기 — delimited / fixed length / custom tokenizer 선택.
targetType()
BeanWrapperFieldSetMapper 자동 적용 — bean property 이름 매칭. Record 또는 setter 필수.
FlatFileItemWriter 의 전체 옵션 그림
@Bean
public FlatFileItemWriter<Customer> customerWriter() {
return new FlatFileItemWriterBuilder<Customer>()
.name("customerWriter")
.resource(new FileSystemResource("out.csv"))
.encoding("UTF-8")
.lineSeparator("\n")
.append(false) // 기존 파일 덮어쓰기
.headerCallback(writer -> writer.write("id,name,email"))
.footerCallback(writer -> writer.write("# end"))
.delimited()
.delimiter(",")
.names("id", "name", "email")
.build();
}
핵심 옵션:
lineSeparator — 줄바꿈
플랫폼별 (\n·\r\n). 명시 권장.
append — 기존 파일 추가 vs 덮어쓰기
true= 기존 내용 보존 + 추가false(default) = 덮어쓰기
headerCallback / footerCallback
파일 시작·끝에 header row·footer row 자동 작성. 19편 Tasklet 패턴 으로 footer 에 통계 박는 케이스에도 활용.
shouldDeleteIfEmpty / shouldDeleteIfExists
shouldDeleteIfEmpty(true)= 처리 item 0건 시 파일 삭제shouldDeleteIfExists(true)= 시작 전 기존 파일 삭제
처리 흐름 시각화
Reader
File "customers.csv"
↓ [FlatFileItemReader]
"1,Alice,alice@example.com" (line 한 줄)
↓ [DelimitedLineTokenizer]
FieldSet["1", "Alice", "alice@..."] (필드 분리)
↓ [BeanWrapperFieldSetMapper]
Customer{id=1, name="Alice", email=...} (도메인 객체)
↓
ItemReader.read() 반환
Writer (반대 방향)
Customer{id=1, name="Alice", email=...}
↓ [BeanWrapperFieldExtractor]
["1", "Alice", "alice@example.com"] (필드 추출)
↓ [DelimitedLineAggregator]
"1,Alice,alice@example.com" (line 합성)
↓ [FlatFileItemWriter]
File "out.csv" 에 append
Reader 의 컴포넌트 customization
@Bean
public FlatFileItemReader<Customer> customerReader() {
FlatFileItemReader<Customer> reader = new FlatFileItemReader<>();
reader.setName("customerReader");
reader.setResource(new FileSystemResource("customers.csv"));
reader.setLineMapper(customLineMapper());
return reader;
}
@Bean
public LineMapper<Customer> customLineMapper() {
DefaultLineMapper<Customer> mapper = new DefaultLineMapper<>();
mapper.setLineTokenizer(customTokenizer());
mapper.setFieldSetMapper(customFieldSetMapper());
return mapper;
}
@Bean
public LineTokenizer customTokenizer() {
DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
tokenizer.setDelimiter(",");
tokenizer.setNames("id", "name", "email");
return tokenizer;
}
@Bean
public FieldSetMapper<Customer> customFieldSetMapper() {
return fieldSet -> new Customer(
fieldSet.readLong("id"),
fieldSet.readString("name"),
fieldSet.readString("email")
);
}
각 컴포넌트를 분리하면 세밀 customization 도 되고 단위 테스트 도 편해져요.
자주 만나는 사고
사고 1: 한글 깨짐
원인 — encoding 미명시 → JVM default 사용 → 환경별 차이.
해결 — .encoding("UTF-8") 명시.
사고 2: Header row 가 데이터로 파싱
원인 — linesToSkip(1) 누락.
해결 — linesToSkip 설정. 또는 header 가 # 시작 = comments("#").
사고 3: Quoted comma 가 구분자로 인식
원인 — "Carol, Jr." 같은 quoted 필드 안 comma 가 구분자로 잘못 처리.
해결 — .quoteCharacter('"') 명시.
사고 4: Multi-line CSV (cell 안 newline)
원인 — "Hello\nWorld" 같은 quoted multi-line.
해결 — DefaultRecordSeparatorPolicy (Spring Batch 가 quote 안 newline 합침).
사고 5: Fixed length 의 padding
원인 — 0001Alice 의 trailing space 가 값에 포함.
해결 — FixedLengthTokenizer.setStrict(false) + Reader 에서 .trim() 또는 명시적 trim 후 mapping.
사고 6: FieldSetMapper 의 missing field
원인 — 컬럼 누락 또는 oder mismatch.
해결 — tokenizer.setNames("id", "name", "email") 의 순서·이름 과 실제 파일 컬럼 일치 확인.
사고 7: BeanWrapperFieldSetMapper 의 type 오류
원인 — "abc" 를 Long id 에 매핑 → BindException.
해결 — 사전 데이터 검증 (skip 정책 또는 ItemProcessor 검증) + custom FieldSetMapper.
운영 권장 패턴
Pattern 1: 표준 CSV reader
@Bean
@StepScope
public FlatFileItemReader<Customer> customerReader(
@Value("#{jobParameters['input.file']}") Resource resource) {
return new FlatFileItemReaderBuilder<Customer>()
.name("customerReader")
.resource(resource)
.encoding("UTF-8")
.linesToSkip(1)
.delimited()
.delimiter(",")
.quoteCharacter('"')
.names("id", "name", "email", "createdAt")
.targetType(Customer.class)
.build();
}
21편 Late Binding (실행 시점에 jobParameters 를 주입) + 표준 CSV reader.
Pattern 2: Multi-format reader (header vs data)
@Bean
public PatternMatchingCompositeLineTokenizer compositeTokenizer() {
Map<String, LineTokenizer> tokenizers = new HashMap<>();
tokenizers.put("HEADER*", headerTokenizer());
tokenizers.put("DATA*", dataTokenizer());
PatternMatchingCompositeLineTokenizer tokenizer =
new PatternMatchingCompositeLineTokenizer();
tokenizer.setTokenizers(tokenizers);
return tokenizer;
}
HEADER* 시작 = header tokenizer, DATA* 시작 = data tokenizer. 복합 포맷 파일 처리.
Pattern 3: Validation 강화 reader
@Bean
public FieldSetMapper<Customer> validatingMapper() {
return fieldSet -> {
Long id = fieldSet.readLong("id");
if (id <= 0) throw new IllegalStateException("Invalid id");
return new Customer(id, fieldSet.readString("name"), fieldSet.readString("email"));
};
}
mapper 안에서 예외 throw → 14편 skip 로직 으로 자연스럽게 흡수.
Pattern 4: Output footer 에 통계
@Bean
public FlatFileItemWriter<Customer> writerWithFooter() {
return new FlatFileItemWriterBuilder<Customer>()
.name("writerWithFooter")
.resource(new FileSystemResource("out.csv"))
.delimited()
.delimiter(",")
.names("id", "name", "email")
.headerCallback(w -> w.write("# Customer Export"))
.footerCallback(w -> w.write("# End — " + LocalDateTime.now()))
.build();
}
시험 직전 한 번 더 — Flat File Overview 함정 압축 노트
- Flat file 두 타입 = Delimited (구분자) / Fixed Length (고정 길이)
- Delimited —
,·|·\t같은 구분자 + quote 처리 - Fixed Length — position 기반, padding 으로 길이 맞춤
- Schema 가 파일 안에 없음 → 별도 합의 문서 필수
- 파싱 3총사 = LineMapper · LineTokenizer · FieldSetMapper
- LineMapper =
mapLine(String line, int lineNumber)→ 도메인 객체 - 표준 =
DefaultLineMapper(Tokenizer + FieldSetMapper 묶음) - LineTokenizer 3종 =
DelimitedLineTokenizer·FixedLengthTokenizer·PatternMatchingCompositeLineTokenizer - FieldSetMapper =
BeanWrapperFieldSetMapper·RecordFieldSetMapper(Java Record) ·PassThroughFieldSetMapper· Custom - Reader 옵션 = name · resource · encoding · linesToSkip · comments · recordSeparatorPolicy · delimited / fixedLength · names · targetType
- Writer 옵션 = name · resource · encoding · lineSeparator · append · headerCallback · footerCallback · shouldDeleteIfEmpty · shouldDeleteIfExists
- headerCallback / footerCallback = 파일 시작/끝 자동 작성
- shouldDeleteIfEmpty(true) = 0건 처리 시 파일 삭제
- Reader 흐름 = File → line → FieldSet → 도메인 객체
- Writer 흐름 = 도메인 객체 → FieldExtractor → LineAggregator → line → file
BeanWrapperFieldExtractor(Writer 측) ·BeanWrapperFieldSetMapper(Reader 측) 대칭DelimitedLineAggregator(Writer 측) ·DelimitedLineTokenizer(Reader 측) 대칭- 함정 — encoding 미명시 → 한글 깨짐 →
.encoding("UTF-8") - 함정 — header row 처리 →
linesToSkip(1) - 함정 — quoted comma →
.quoteCharacter('"') - 함정 — multi-line CSV →
DefaultRecordSeparatorPolicy - 함정 — fixed length padding → trim 처리
- 함정 — BeanWrapper type 오류 → 검증·skip 또는 custom mapper
- 패턴 — 표준 CSV reader + Late Binding
- 패턴 — multi-format (
PatternMatchingCompositeLineTokenizer) - 패턴 — validating mapper + skip 흡수
- 패턴 — footer 에 통계
- 단순 처리는 Builder, 복잡 customization 은 컴포넌트 분리 권장
- Part 6 시작 — 다음 글부터 각 컴포넌트 깊이
공식 문서: Flat Files 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 22편 — ItemReader 인터페이스 종합 · Delegate Pattern
- 23편 — ItemWriter 인터페이스 종합
- 24편 — ItemStream 인터페이스 본격 풀이
- 25편 — Reader · Writer 구현체 카탈로그
- 26편 — Custom Reader · Writer 직접 구현
다음 글: