[파일처리] 4. FlatFileItemWriter 다양한 쓰기 전략

y001·2026년 3월 8일

Spring Batch Guide

목록 보기
14/19
post-thumbnail

1. 시작하면서

이전 글에서는 FlatFileItemWriter의 내부 구조를 살펴보았다. 핵심은 단순하다. FlatFileItemWriter는 객체를 직접 파일에 기록하지 않고 먼저 객체를 문자열 한 줄로 변환한 뒤 그 문자열을 파일에 기록한다. 즉 Writer의 역할은 객체 → 문자열 → 파일이라는 변환 과정을 수행하는 것이다. 출력 형식이 CSV이든 사람이 읽는 보고서 형식이든 JSON 문자열이든 이 구조는 변하지 않는다. 달라지는 것은 객체를 문자열로 만드는 방식뿐이다. 이번 글에서는 FlatFileItemWriter를 사용할 때 가장 자주 등장하는 출력 전략 네 가지를 살펴본다. CSV 파일 출력, 포맷 문자열 출력, 커스텀 문자열 출력, 그리고 여러 파일로 분할하는 방식이다.

2. FlatFileItemWriterBuilder, CSV 파일로 쓰기

가장 기본적인 출력 방식은 CSV이다. Spring Batch에서는 delimited() 설정을 사용해 구분자 기반 문자열을 생성할 수 있다. 예를 들어 다음과 같은 객체가 있다고 가정해 보자.

public class ErrorLog {
    private String errorId;
    private String occurredAt;
    private String severity;
    private String message;
}

이 객체를 CSV 파일로 기록하려면 FlatFileItemWriterBuilder를 이용해 다음과 같이 Writer를 구성한다.

@Bean
public FlatFileItemWriter<ErrorLog> errorLogWriter() {
    return new FlatFileItemWriterBuilder<ErrorLog>()
            .name("errorLogWriter")
            .resource(new FileSystemResource("error_logs.csv"))
            .delimited()
            .delimiter(",")
            .names("errorId", "occurredAt", "severity", "message")
            .headerCallback(writer -> writer.write("errorId,occurredAt,severity,message"))
            .build();
}

delimited()는 구분자 기반 출력 전략을 사용하겠다는 의미이며 내부적으로 DelimitedLineAggregator가 사용된다. names()는 객체에서 어떤 필드를 어떤 순서로 추출할지 정의한다. 이 순서가 그대로 CSV 컬럼 순서가 된다. 예를 들어 다음 객체가 Writer로 전달되면

new ErrorLog(
    "ERR-001",
    "2024-01-25 10:15:23",
    "CRITICAL",
    "Database connection failure"
)

출력 파일은 다음과 같은 형태가 된다.

errorId,occurredAt,severity,message
ERR-001,2024-01-25 10:15:23,CRITICAL,Database connection failure

3. formatted, 포맷 문자열로 쓰기

항상 CSV처럼 단순히 구분자로 연결하는 방식만 필요한 것은 아니다. 운영 보고서나 로그 파일처럼 사람이 읽기 쉬운 문장 형식으로 데이터를 출력해야 하는 경우도 있다. 이런 상황에서는 formatted() 설정을 사용해 포맷 문자열 기반 출력 전략을 사용할 수 있다.

@Bean
public FlatFileItemWriter<ErrorLog> errorLogReportWriter() {
    return new FlatFileItemWriterBuilder<ErrorLog>()
            .name("errorLogReportWriter")
            .resource(new FileSystemResource("error_report.txt"))
            .formatted()
            .format("[%s] %s - %s")
            .names("severity", "errorId", "message")
            .headerCallback(writer -> writer.write("===== Error Report ====="))
            .footerCallback(writer -> writer.write("===== End Of Report ====="))
            .build();
}

names()에서 정의한 필드 순서는 format() 문자열의 %s 위치와 대응된다. 예를 들어 severity, errorId, message 순서로 값을 추출하면 [%s] %s - %s 포맷에 맞춰 문자열이 만들어진다. 따라서 같은 객체가 입력되면 결과 파일은 다음과 같은 형태가 된다.

===== Error Report =====
[CRITICAL] ERR-001 - Database connection failure
===== End Of Report =====

4. lineAggregator, 커스텀 형식으로 쓰기

CSV나 포맷 문자열만으로 해결되지 않는 경우도 있다. 예를 들어 로그 데이터를 JSON 형태로 한 줄씩 저장하는 JSONL 형식이 있다. 이런 경우에는 lineAggregator()를 직접 구현해 원하는 문자열을 생성할 수 있다.

@Bean
public FlatFileItemWriter<ErrorLog> jsonWriter(ObjectMapper objectMapper) {
    return new FlatFileItemWriterBuilder<ErrorLog>()
            .name("jsonWriter")
            .resource(new FileSystemResource("error_logs.jsonl"))
            .lineAggregator(item -> {
                try {
                    return objectMapper.writeValueAsString(item);
                } catch (JsonProcessingException e) {
                    throw new RuntimeException(e);
                }
            })
            .build();
}

이 설정에서는 lineAggregator()가 객체를 JSON 문자열로 변환한다. Writer는 생성된 문자열을 그대로 파일에 기록한다. 따라서 결과 파일은 다음과 같은 JSONL 형식이 된다.

{"errorId":"ERR-001","occurredAt":"2024-01-25 10:15:23","severity":"CRITICAL","message":"Database connection failure"}
{"errorId":"ERR-002","occurredAt":"2024-01-25 10:16:02","severity":"ERROR","message":"Memory overflow"}

lineAggregator()를 직접 구현하면 객체를 어떤 문자열 형식으로도 변환할 수 있다.

5. MultiResourceItemWriter, 여러 파일로 나눠 쓰기

데이터 양이 많아지면 하나의 파일이 지나치게 커질 수 있다. 이때는 MultiResourceItemWriter를 사용해 데이터를 여러 파일로 분할할 수 있다. 중요한 점은 MultiResourceItemWriter가 문자열을 만드는 Writer가 아니라는 것이다. 실제 한 줄 문자열을 생성하는 역할은 delegate로 전달한 FlatFileItemWriter가 담당하고, 자신은 파일 분할만 관리한다.

@Bean
public MultiResourceItemWriter<ErrorLog> multiWriter() {
    return new MultiResourceItemWriterBuilder<ErrorLog>()
            .name("multiWriter")
            .resource(new FileSystemResource("error_log"))
            .itemCountLimitPerResource(2)
            .delegate(errorLogWriter())
            .resourceSuffixCreator(index -> String.format("_%03d.csv", index))
            .build();
}

이 설정에서는 한 파일에 최대 두 건의 데이터만 기록된다. 만약 데이터가 다섯 건이라면 세 개의 파일이 생성된다.여기서 파일 분할은 MultiResourceItemWriter가 담당하고 실제 출력 형식은 delegate Writer가 담당한다.

error_log_001.csv
error_log_002.csv
error_log_003.csv

0개의 댓글