이전 장에서 FlatFileItemReader를 통해 시스템의 기록을 읽어내는 법을 배웠다. 읽기가 끝났다면 이제는 처리된 결과를 우리가 원하는 형식으로 기록할 차례다. 스프링 배치는 데이터를 플랫 파일(CSV, TXT, JSONL 등)로 쓰는 작업을 위해 FlatFileItemWriter라는 강력한 무기를 제공한다.
[Spring Batch] 시스템의 기록을 읽어내는 FlatFileItemReader
실무에서 여전히 가장 많이 다루는 데이터 소스 중 하나는 파일이다. 금융 거래 내역이 담긴 CSV, 기관 간 데이터 교환에 쓰이는 고정 길이(Fixed-Length) 파일, 그리고 서버의 복잡한 로그 파일들이
myblog01150.tistory.com
단순히 파일에 텍스트를 적는 저수준의 작업을 넘어, 버퍼링을 통한 트랜잭션 관리, 대용량 데이터의 파일 분할 저장, OS 레벨의 동기화 설정 등 실무에서 필수적인 기능들을 어떻게 구성하고 활용하는지 상세히 정리해 본다.
1. 파일 기록의 핵심: 내부 아키텍처와 2단계 작전
FlatFileItemWriter는 도메인 객체를 한 줄의 문자열로 변환하여 파일에 기록한다. 이 과정은 단순히 toString()을 호출하는 것이 아니라, 필드 추출(Extraction)과 라인 결합(Aggregation)이라는 정교한 2단계 공정으로 수행된다.

🔍 데이터 변환 프로세스
- FieldExtractor(필드 추출): 도메인 객체에서 파일에 기록할 필드 값들을 뽑아내어 Object[] 배열로 변환한다.
- LineAggregator(라인 결합): 추출된 필드들을 구분자(,)로 잇거나 특정 포맷(String.format)에 맞춰 하나의 완성된 문자열로 합친다.
2. FieldExtractor와 LineAggregator의 협업 모델
스프링 배치는 객체의 타입과 출력 형식에 따라 다양한 구현체를 제공하며, 이를 유기적으로 조합한다.
🛠️ FieldExtractor: 객체에서 값을 꺼내는 자
- BeanWrapperFieldExtractor: 일반 Java Bean(Getter/Setter 기반) 객체에서 프로퍼티 이름을 기준으로 값을 추출한다.
- RecordFieldExtractor: Java Record 객체에서 컴포넌트의 Accessor 메서드를 호출하여 값을 추출한다.
🛠️ LineAggregator: 문자열로 결합하는 자
- DelimitedLineAggregator: 추출된 필드들을 특정 구분자로 연결한다. (주로 CSV 생성 시 사용)
- FormatterLineAggregator: String.format()과 유사한 포맷 문자열을 사용하여 유연하게 행을 구성한다.
3. 실전: 구분자 기반의 CSV 파일 쓰기
가장 보편적인 CSV 형식을 FlatFileItemWriterBuilder로 구성해 본다.
💻 실전 코드: 처형 명단(DeathNote) 기록
@Bean
@StepScope
public FlatFileItemWriter<DeathNote> deathNoteWriter(
@Value("#{jobParameters['outputPath']}") String outputPath) {
return new FlatFileItemWriterBuilder<DeathNote>()
.name("deathNoteWriter")
.resource(new FileSystemResource(outputPath + "/death_notes.csv"))
.delimited() // [핵심] 구분자 모드 활성화 (DelimitedLineAggregator 사용)
.delimiter(",") // 구분자 지정 (기본값 ",")
.sourceType(DeathNote.class) // [중요] 필드 추출기 자동 구성을 위해 명시
.names("threadId", "victimName", "description", "executionMethod") // 기록할 필드 순서
.headerCallback(writer -> writer.write("스레드ID,처형대상,처형사유,처형방식")) // 헤더 작성
.footerCallback(writer -> writer.write("이상 기록 완료.")) // 푸터 작성
.build();
}
- 자동 구성의 묘미: sourceType()에 record 클래스를 넘기면 배치가 이를 감지해 자동으로 RecordFieldExtractor를 배정한다.
- 커스텀 필드 추출: 만약 필드 두 개를 합쳐서 출력하고 싶다면 .fieldExtractor(item -> new Object[]{ item.id() + ":" + item.name() })와 같이 람다로 직접 구현할 수 있다.
4. 파일 처리의 세밀한 옵션과 안정성 전략
존재하는 파일의 운명을 결정하고, 데이터 유실을 방지하기 위한 핵심 옵션들이다.
⚙️ 파일 제어 옵션
- shouldDeleteIfExists(true): 파일이 이미 존재하면 삭제하고 새로 만든다. false일 때 파일이 이미 있으면 ItemStreamException이 발생한다.
- append(true): 기존 파일 끝에 데이터를 덧붙인다. 이 경우 headerCallback은 무시된다.
- shouldDeleteIfEmpty(true): 결과 데이터가 0건이면 생성된 빈 파일을 자동 삭제하여 시스템을 깨끗하게 유지한다.
⚙️ 트랜잭션과 OS 캐시 제어
- transactional(true): 기본값이다. 데이터를 즉시 파일에 쓰지 않고 내부 버퍼에 담아두었다가, 청크가 성공하여 커밋되는 직전(beforeCommit)에 파일로 쏟아낸다. 롤백 시 파일에도 아무 기록이 남지 않으므로 DB와 정합성을 맞출 수 있다.
- forceSync(true): 데이터를 OS 메모리 캐시가 아닌 디스크에 즉시 동기화한다. 성능은 다소 떨어지지만, 갑작스러운 전원 차단 시에도 데이터 유실 위험을 최소화한다.
5. MultiResourceItemWriter: 대용량 데이터 분할
1,000만 건의 데이터를 하나의 파일에 담으면 분석과 전송이 불가능하다. 이때 위임(Delegation) 패턴을 사용하여 파일을 쪼개어 저장한다.

@Bean
public MultiResourceItemWriter<DeathNote> multiResourceWriter() {
return new MultiResourceItemWriterBuilder<DeathNote>()
.name("multiResourceWriter")
.resource(new FileSystemResource("data/output/death_report")) // 기본 경로 및 이름
.itemCountLimitPerResource(5000) // 5000건마다 새 파일 생성
.delegate(delegateWriter()) // 실제 파일 쓰기를 수행할 FlatFileItemWriter 위임
.resourceSuffixCreator(index -> String.format("_%03d.txt", index)) // 접미사 규칙
.build();
}
6. 실무 시나리오: 분산 웹서버 로그 통합
3개의 원격 웹서버에서 쏟아지는 CSV 로그를 수집하여 가공한 뒤, 최종 보고서를 JSONL(JSON Lines) 형식으로 저장하는 복합 자동화 Job이다.
🛠️ 구성 단계
- 패키지 준비: SystemCommandTasklet을 사용하여 날짜별 디렉토리를 생성(mkdir -p)한다.
- 로그 수집: 원격 서버의 로그를 로컬로 수집(실무에선 scp, 예제에선 cp)한다.
- 로그 처리: MultiResourceItemReader로 여러 로그를 읽고, ItemProcessor로 에러 코드를 추출한 뒤, FlatFileItemWriter로 JSON 직렬화하여 기록한다.
💻 핵심 코드: JSON 포맷 라이터 구현
@Bean
@StepScope
public FlatFileItemWriter<TerminatedLogEntry> executedLogWriter(ObjectMapper objectMapper) {
return new FlatFileItemWriterBuilder<TerminatedLogEntry>()
.name("executedLogWriter")
.resource(new FileSystemResource("executed_logs/report.jsonl"))
// [핵심] LineAggregator를 람다로 구현하여 ObjectMapper로 객체를 JSON 문자열화
.lineAggregator(item -> {
try {
return objectMapper.writeValueAsString(item);
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON 변환 실패", e);
}
})
.build();
}
7. 마무리하며:
이번 장에서는 데이터를 영구적으로 각인시키는 FlatFileItemWriter를 알아보았다.
- 구조의 이해: FieldExtractor와 LineAggregator의 위임 관계를 통해 배치의 유연한 설계를 파악했다.
- 안전성 확보: 버퍼링 기반의 트랜잭션 관리와 파일 처리 옵션으로 시스템 장애 시나리오에 대비했다.
- 대규모 확장: MultiResource 패턴을 통해 물리적 한계를 넘어서는 대용량 데이터 처리 기법을 익혔다.
'Spring > Batch' 카테고리의 다른 글
| [Spring Batch] RDB 대용량 데이터 처리의 정석: JDBC vs JPA 분석 (1) | 2026.05.14 |
|---|---|
| [Spring Batch] 스프링 배치 JSON 읽기/쓰기 전략 (0) | 2026.05.11 |
| [Spring Batch] 시스템의 기록을 읽어내는 FlatFileItemReader (0) | 2026.05.09 |
| [Spring Batch] Listener를 통한 전처리 및 후처리 (0) | 2026.05.08 |
| [Spring Batch] 잡 파라미터와 Scope로 제어하는 배치 생명주기 (0) | 2026.05.07 |