Spring Batch 입문 33편 — Multi-File Input · MultiResourceItemReader

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

Spring Batch 입문 33편. 한 Step 에서 여러 파일을 묶어 처리하는 MultiResourceItemReader — wildcard resource pattern (`classpath:data/file-*.txt`), delegate 패턴 (FlatFile · XML · JSON 재사용), setComparator 의 재시작 안전성, 작업 디렉토리 격리 권장, partitioning 결합·CompositeItemReader (Spring Batch 6) 비교까지 정리한 학습 노트. Part 6 의 file 시리즈 마무리.

📚 Spring Batch 입문에서 운영까지 · 33편 — Multi-File Input · MultiResourceItemReader

이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 33편이에요. 32편 까지 파일 포맷별 reader/writer 를 봤다면, 이번 33편은 한 Step 에서 여러 파일을 묶어 처리하는 패턴 — MultiResourceItemReader. Part 6 의 file 시리즈 마무리.

왜 여러 파일을 한 Step 으로?

운영에서 흔히 마주치는 case 는 이런 모양이에요. 외부 시스템이 일자별 파일 로 데이터를 주고 한 batch run 에서 주간 7일치 를 한 번에 돌려야 하거나, 데이터를 작은 파일 N개 로 분할 전송받아 모두 read 해야 하거나, partition 별 결과 파일 을 합쳐야 하거나, 여러 지역 사이트 의 daily CSV 를 함께 처리해야 하는 상황이죠.

각 파일을 별도 Step 으로 짜면 Step 수 폭발 이 일어나요. 한 Step + 여러 파일 이 정답.

MultiResourceItemReader — 핵심 동작

@Bean
public MultiResourceItemReader<Foo> multiReader(
        @Value("classpath:data/input/file-*.txt") Resource[] resources,
        FlatFileItemReader<Foo> delegate) {
    return new MultiResourceItemReaderBuilder<Foo>()
        .name("multiReader")
        .resources(resources)
        .delegate(delegate)
        .build();
}

작동 흐름:

1. resources = [file-1.txt, file-2.txt, file-3.txt]
2. 첫 resource (file-1.txt) → delegate 가 open + read 시작
3. delegate.read() == null → 다음 resource (file-2.txt) 로 전환
4. file-2.txt 의 read 끝 → file-3.txt
5. 모든 resource read 끝 → null (Step 종료)

N 개 파일을 1 개의 논리 stream 으로.

Wildcard Pattern — Spring Resource

@Value("classpath:data/input/file-*.txt") Resource[] resources

Spring 의 Resource[] 주입 = wildcard pattern 자동 해석.

자주 쓰는 패턴:

패턴 매칭
classpath:data/file-*.txt classpath 안 file-*.txt 매칭
file:/data/input/*.csv 절대 경로
file:/data/2026-05-*/orders.csv 디렉토리 패턴
classpath*:META-INF/data/*.txt 모든 classpath jar 에서

?·*·** 모두 사용 가능. classpath*: prefix = 여러 jar 에서 동시 검색.

Delegate Pattern 재사용

MultiResourceItemReader어떤 ItemReader 도 delegate 로 받음:

Delegate 사용
FlatFileItemReader (CSV·TSV 등 평문 파일 reader) CSV/TSV 여러 파일
StaxEventItemReader (StAX 기반 XML reader) XML 여러 파일
JsonItemReader (Jackson·Gson 기반 JSON reader) JSON 여러 파일
JdbcCursorItemReader (X — Resource 기반 아님)

→ Reader 가 ResourceAwareItemReaderItemStream (resource 주입 + 재시작 위치 추적 인터페이스) 구현 시 동작. 표준 파일 reader 들 모두 구현.

결합 예제 — CSV

@Bean
public FlatFileItemReader<Customer> csvDelegate() {
    return new FlatFileItemReaderBuilder<Customer>()
        .name("csvDelegate")
        .delimited()
            .delimiter(",")
            .names("id", "name", "email")
        .targetType(Customer.class)
        .build();
    // resource 는 MultiResource 가 주입
}

@Bean
@StepScope
public MultiResourceItemReader<Customer> multiCsvReader(
        @Value("#{jobParameters['input.pattern']}") Resource[] resources,
        FlatFileItemReader<Customer> csvDelegate) {
    return new MultiResourceItemReaderBuilder<Customer>()
        .name("multiCsvReader")
        .resources(resources)
        .delegate(csvDelegate)
        .build();
}

delegate 의 resource 는 비워 둠MultiResourceItemReaderresource 마다 setter 로 주입.

결합 예제 — XML

@Bean
public StaxEventItemReader<Trade> xmlDelegate() {
    return new StaxEventItemReaderBuilder<Trade>()
        .name("xmlDelegate")
        .addFragmentRootElements("trade")
        .unmarshaller(tradeMarshaller())
        .build();
}

@Bean
@StepScope
public MultiResourceItemReader<Trade> multiXmlReader(
        @Value("file:/data/trades-*.xml") Resource[] resources,
        StaxEventItemReader<Trade> xmlDelegate) {
    return new MultiResourceItemReaderBuilder<Trade>()
        .name("multiXmlReader")
        .resources(resources)
        .delegate(xmlDelegate)
        .build();
}

XML 도 똑같은 패턴.

재시작 안전성 — setComparator

여기서 시험 함정이 하나 있어요.

Resource[] resources = ...;       // OS 가 반환하는 순서가 일정한가?

파일 시스템의 기본 순서가 보장 X. OS·filesystem 별로 다름. 재시작 시 순서가 바뀌면 이미 처리한 파일을 다시 처리하거나 건너뛰는 사고.

해결setComparator 로 순서 명시:

@Bean
public MultiResourceItemReader<Customer> multiReader(
        Resource[] resources, FlatFileItemReader<Customer> delegate) {
    MultiResourceItemReader<Customer> reader = new MultiResourceItemReader<>();
    reader.setName("multiReader");
    reader.setResources(resources);
    reader.setDelegate(delegate);
    reader.setComparator(Comparator.comparing(Resource::getFilename));   // ★
    return reader;
}

Builder 의 기본 동작Comparator 미지정 시 file name 알파벳 순 (Spring Batch 의 default).

Input resources are ordered by using MultiResourceItemReader#setComparator(Comparator) to make sure resource ordering is preserved between job runs in restart scenario. — 공식 reference

명시적 Comparator 설정 권장 — 운영 안정성.

작업 디렉토리 격리 권장

It is recommended that batch jobs work with their own individual directories until completed successfully. — 공식 reference

— 처리 중 새 파일이 추가 되면 Resource[] 의 내용이 변경 → 재시작 시 다른 파일 set.

권장 흐름은 이래요. 처리 시작 전 에 입력 디렉토리에서 작업 디렉토리로 파일을 이동 시키고, 이후 read 는 작업 디렉토리 에서만 한 뒤, 처리 완료 후 다시 archive 디렉토리 로 옮긴다.

19편 TaskletStep (chunk 가 아닌 단발 작업 Step) 으로 이 파일 이동 단계 를 batch 의 일부로.

패턴 예제

@Bean
public Job multiFileJob(JobRepository repo, Step moveFilesStep,
                        Step processStep, Step archiveStep) {
    return new JobBuilder("multiFileJob", repo)
        .start(moveFilesStep)        // Tasklet: input → working/
        .next(processStep)           // Chunk: working/ 의 모든 파일 처리
        .next(archiveStep)           // Tasklet: working/ → archive/
        .build();
}

ExecutionContext 추적

MultiResourceItemReader 가 추적하는 상태는 두 가지예요 — 현재 처리 중인 resource index, 그리고 delegate 의 현재 위치 (delegate ExecutionContext (Step·Job 단위 상태 저장소)).

→ 재시작 시 같은 파일 같은 위치부터. 17편·24편 ItemStream (재시작 위치 저장·복구 인터페이스) 메커니즘 적용.

ExecutionContext key

ExecutionContext 에 저장 시 MultiResourceItemReader prefix. delegate 의 key 와 충돌 방지.

Writer 측 대칭 — MultiResourceItemWriter

25편에서 본 대량 출력 분할:

@Bean
public MultiResourceItemWriter<Customer> multiWriter(
        FlatFileItemWriter<Customer> delegate) {
    return new MultiResourceItemWriterBuilder<Customer>()
        .name("multiWriter")
        .delegate(delegate)
        .resource(new FileSystemResource("output/customers"))
        .resourceSuffixCreator(index -> "-" + index + ".csv")
        .itemCountLimitPerResource(10_000)
        .build();
}

Reader = 여러 파일 → 1 stream, Writer = 1 stream → 여러 파일. 역방향 대칭.

CompositeItemReader (Spring Batch 6) 비교

Spring Batch 6 에서 새로 추가 — CompositeItemReader:

@Bean
public CompositeItemReader<Customer> compositeReader(
        FlatFileItemReader<Customer> csvReader,
        JsonItemReader<Customer> jsonReader) {
    return new CompositeItemReader<>(List.of(csvReader, jsonReader));
}

MultiResource vs Composite 비교

항목 MultiResourceItemReader CompositeItemReader
입력 형식 동일 형식 여러 파일 서로 다른 reader 순차
사용 case CSV 7일치 / XML 여러 partition CSV + JSON 결합
Delegate 수 1개 (resource 만 여러) N개 (각 reader 가 자기 resource)
wildcard pattern X (각 reader 직접 설정)

MultiResource = 동일 포맷·여러 파일, Composite = 다른 포맷·여러 reader.

Partitioning 결합

37편 Partitioning 환경 — 각 partition 이 다른 resource 처리:

@Bean
public Partitioner filePartitioner(@Value("file:/data/*.csv") Resource[] resources) {
    MultiResourcePartitioner partitioner = new MultiResourcePartitioner();
    partitioner.setResources(resources);
    return partitioner;
}

MultiResourcePartitioner (Spring Batch 가 제공하는 파일 단위 Partitioner) = 각 partition Step 에 서로 다른 파일 할당. 각 thread 가 자기 파일만 처리true parallel.

@Bean
public Step workerStep(JobRepository repo, PlatformTransactionManager tx,
                       FlatFileItemReader<Customer> reader,
                       JdbcBatchItemWriter<Customer> writer) {
    return new StepBuilder("workerStep", repo)
        .<Customer, Customer>chunk(100, tx)
        .reader(reader)              // @StepScope 로 partition 의 resource 주입
        .writer(writer)
        .build();
}

@Bean
@StepScope
public FlatFileItemReader<Customer> reader(
        @Value("#{stepExecutionContext['fileName']}") Resource resource) {
    return new FlatFileItemReaderBuilder<Customer>()
        .name("partitionReader")
        .resource(resource)
        // ...
        .build();
}

MultiResourcePartitionerstepExecutionContext['fileName']각 partition 의 resource 주입. 21편 Late Binding (실행 시점에 jobParameter·stepContext 값을 주입하는 방식) 활용.

자주 만나는 사고

사고 1: 재시작 시 다른 파일 처리

원인 — Comparator 미지정 + 파일 시스템 순서 변경.

해결setComparator(Comparator.comparing(Resource::getFilename)) 명시.

사고 2: 처리 중 새 파일 추가

원인 — 입력 디렉토리에 실시간 새 파일 들어옴.

해결작업 디렉토리 격리 — 처리 시작 시점 snapshot.

사고 3: 0건 wildcard 매칭

원인classpath:data/*.txt 가 0개 매칭.

해결setStrict(false) (default 가 false 라 그냥 빈 처리, 단 일관성 위해 명시 권장).

사고 4: delegate 의 ItemStream 누락

원인 — delegate 가 ItemStream 미구현 — 재시작 시 위치 복구 X.

해결 — delegate 는 반드시 ResourceAwareItemReaderItemStream 구현. 표준 reader 들 모두 OK.

사고 5: delegate name 충돌

원인 — 두 MultiResourceItemReader 가 같은 delegate 공유 (singleton).

해결 — 각 MultiResourceItemReader독립 delegate. 또는 @StepScope.

사고 6: classpath wildcard 가 jar 안 못 봄

원인classpath:단일 location 만 — multiple jar 검색 X.

해결classpath*: (별표 추가) — 모든 jar 검색.

사고 7: 너무 많은 파일

원인 — wildcard 가 수만 개 파일 매칭 → 메모리·시간 폭발.

해결partitioning 으로 분산 또는 batch 분할 (오늘 분량만).

운영 권장 패턴

Pattern 1: 표준 CSV 다중 파일

@Bean
public FlatFileItemReader<Customer> csvDelegate() {
    return new FlatFileItemReaderBuilder<Customer>()
        .name("csvDelegate")
        .delimited()
            .delimiter(",")
            .names("id", "name", "email")
        .targetType(Customer.class)
        .build();
}

@Bean
@StepScope
public MultiResourceItemReader<Customer> dailyCsvReader(
        @Value("#{jobParameters['input.pattern']}") Resource[] resources,
        FlatFileItemReader<Customer> csvDelegate) {
    MultiResourceItemReader<Customer> reader = new MultiResourceItemReader<>();
    reader.setName("dailyCsvReader");
    reader.setResources(resources);
    reader.setDelegate(csvDelegate);
    reader.setComparator(Comparator.comparing(Resource::getFilename));
    return reader;
}

Late Binding 으로 jobParameter pattern 받음. comparator 명시.

Pattern 2: 일자별 디렉토리 패턴

java -jar batch.jar input.pattern="file:/data/2026-05-{17,18,19}/orders.csv"

특정 일자만 처리. Spring resource pattern 의 brace expansion.

Pattern 3: 작업 디렉토리 격리

@Bean
public Step moveInputFiles(JobRepository repo, PlatformTransactionManager tx) {
    return new StepBuilder("moveInputFiles", repo)
        .tasklet((contribution, chunkContext) -> {
            Path inputDir = Paths.get("/data/input");
            Path workDir = Paths.get("/data/working");
            Files.list(inputDir)
                .filter(p -> p.getFileName().toString().endsWith(".csv"))
                .forEach(p -> {
                    try { Files.move(p, workDir.resolve(p.getFileName())); }
                    catch (IOException e) { throw new RuntimeException(e); }
                });
            return RepeatStatus.FINISHED;
        }, tx)
        .build();
}

Tasklet 으로 처리 시작 전 파일 이동 — 격리.

Pattern 4: Partitioning 결합

@Bean
public Step masterStep(JobRepository repo, PartitionHandler handler) {
    return new StepBuilder("masterStep", repo)
        .partitioner("workerStep", filePartitioner(null))
        .partitionHandler(handler)
        .build();
}

MultiResourcePartitioner + worker step = 진정한 parallel.

Pattern 5: 처리 후 archive

@Bean
public Step archiveStep(JobRepository repo, PlatformTransactionManager tx) {
    return new StepBuilder("archiveStep", repo)
        .tasklet((contribution, chunkContext) -> {
            Path workDir = Paths.get("/data/working");
            Path archiveDir = Paths.get("/data/archive/" + LocalDate.now());
            Files.createDirectories(archiveDir);
            Files.list(workDir)
                .forEach(p -> {
                    try { Files.move(p, archiveDir.resolve(p.getFileName())); }
                    catch (IOException e) { throw new RuntimeException(e); }
                });
            return RepeatStatus.FINISHED;
        }, tx)
        .build();
}

처리 완료 후 날짜별 archive. 재실행 시 입력 깨끗.

시험 직전 한 번 더 — Multi-File Input 함정 압축 노트

  • MultiResourceItemReader = 여러 파일을 1 논리 stream 으로
  • 작동 — 첫 resource read → null → 다음 resource → ... → 마지막 null = Step 종료
  • delegate = ResourceAwareItemReaderItemStream 구현 reader (FlatFile · Stax · Json)
  • delegate 의 resource 는 비워 둠 — MultiResource 가 setter 주입
  • Wildcard pattern = Spring Resource[] 자동 해석
  • 패턴 — classpath:·classpath*:·file: + ?·*·**·{a,b}
  • setComparator = 재시작 안전성의 핵심 — 파일 순서 보장
  • default = file name 알파벳 순 (Builder default), 명시 권장
  • ExecutionContext = 현재 resource index + delegate 위치
  • 작업 디렉토리 격리 = 처리 중 새 파일 추가 방지
  • 흐름 — TaskletStep (move input→working) → process → TaskletStep (working→archive)
  • MultiResourceItemWriter (25편) = 역방향 대칭 — 1 stream → 여러 파일
  • CompositeItemReader (Spring Batch 6) = 서로 다른 reader 순차
  • MultiResource vs Composite — 동일 포맷·여러 파일 vs 다른 포맷·여러 reader
  • Partitioning 결합 = MultiResourcePartitioner + worker step + @StepScope reader
  • 각 partition 의 resource = stepExecutionContext['fileName'] (21편 Late Binding)
  • 함정 — Comparator 누락 → 재시작 다른 파일 처리
  • 함정 — 처리 중 새 파일 추가 → snapshot 격리
  • 함정 — 0건 매칭 → strict 옵션 또는 사전 검증
  • 함정 — delegate ItemStream 누락 → 재시작 위치 복구 X
  • 함정 — singleton delegate 두 reader 공유 → @StepScope 또는 독립
  • 함정 — classpath 가 multiple jar 못 봄 → classpath*:
  • 함정 — 수만 파일 매칭 → partitioning 분산 또는 batch 분할
  • 패턴 — Late Binding pattern 으로 jobParameter
  • 패턴 — 일자별 디렉토리 (brace expansion)
  • 패턴 — 작업 디렉토리 격리 + archive
  • 패턴 — partitioning 으로 parallel
  • Part 6 file 시리즈 마무리 — 다음 글부터 DB reader/writer

공식 문서: Multi-File Input 에서 원문을 확인할 수 있어요.

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

이전 글:

다음 글:

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

답글 남기기

error: Content is protected !!