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 입문에서 운영까지 시리즈 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 는 비워 둠 — MultiResourceItemReader 가 resource 마다 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();
}
MultiResourcePartitioner 가 stepExecutionContext['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 +@StepScopereader - 각 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 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 28편 — FieldSet · Flat File 의 ResultSet
- 29편 — FlatFileItemReader 깊은 옵션
- 30편 — FlatFileItemWriter · LineAggregator · FieldExtractor
- 31편 — XML Reader · Writer · StAX 기반 streaming
- 32편 — JSON Reader · Writer · Jackson · Gson
다음 글: