Spring Batch 입문 27편 — Flat File Overview · 파싱 3총사

2026-05-17Spring Batch 입문에서 운영까지

Spring Batch 입문 27편. Flat File (CSV · 고정 길이) 의 두 타입, 파싱 단계의 3총사 — LineMapper · LineTokenizer · FieldSetMapper 의 협력 구조, RecordSeparatorPolicy · linesToSkip · comments · encoding 같은 옵션 그림, 표준 ItemReader/Writer 와의 연결까지 정리한 학습 노트. Part 6 시작.

📚 Spring Batch 입문에서 운영까지 · 27편 — Flat File Overview · 파싱 3총사

이 글은 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 → 도메인 객체 변환.

표준 구현 = DefaultLineMapperLineTokenizer + 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 같은 경우 DefaultRecordSeparatorPolicyquote 안 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 에서 원문을 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!