매일 새벽 2시, 은행 시스템에서는 수백만 개의 거래 내역을 처리합니다.
만약 이 작업을 일반적인 웹 애플리케이션으로 처리한다면?
- 메모리 부족으로 중단됨
- 오류 하나로 전체 거래가 실패
- 재시작 시 처음부터 다시 처리
- 진행 상황을 추적하기 어려움
Spring Batch는 이런 대용량 데이터를 효율적으로 처리하기 위해 태어났습니다. 그런데 처음 배우는 입장에서는 Job, Step, ItemReader, Listener 등 낯선 개념들이 많습니다.
이 글에서는 거래 내역 일괄 처리라는 현실적인 예시를 통해 Spring Batch의 핵심 개념을 차근차근 설명하겠습니다.
1. Job과 Step: 계층적 구조의 이해
Job이란? “전체 작업의 집합”
매일 새벽 2시에 거래 내역을 처리하는 프로세스를 상상해보세요:
- 은행 시스템에서 거래 파일 수신
- 파일을 읽어서 데이터 검증
- 데이터베이스에 저장
- 통계 계산
- 최종 리포트 생성
이 “전체 거래 처리 프로세스”를 Job이라고 부릅니다.
Step이란? “Job 내의 개별 작업”
위의 프로세스를 더 자세히 보면, 실제로는 3개의 독립적인 Step으로 나뉩니다:
🎯 Job: 거래 내역 일괄 처리
├─ 📋 Step 1: 거래 데이터 검증 & 저장 (Read → Validate → Write)
├─ 📊 Step 2: 통계 계산
└─ 📄 Step 3: 최종 리포트 생성
각 Step은:
- 순차적으로 실행 (Step 1 → Step 2 → Step 3)
- 독립적으로 실행 가능 (필요하면 특정 Step만 재실행 가능)
- 자신의 상태 관리 (시작/실패/완료 정보 기록)
거래 처리"] --> B["📋 Step 1
검증 & 저장"] B --> C["📊 Step 2
통계 계산"] C --> D["📄 Step 3
리포트 생성"] style A fill:#2d3748,stroke:#90cdf4,stroke-width:2px,color:#e2e8f0 style B fill:#1a202c,stroke:#68d391,stroke-width:2px,color:#e2e8f0 style C fill:#1a202c,stroke:#68d391,stroke-width:2px,color:#e2e8f0 style D fill:#1a202c,stroke:#68d391,stroke-width:2px,color:#e2e8f0
2. Read → Process → Write: 기본 흐름의 이해
Step 1 “거래 데이터 검증 & 저장”은 Spring Batch의 핵심 패턴인 ItemReader → ItemProcessor → ItemWriter 구조를 따릅니다.
ItemReader: 데이터를 읽다
@Bean
public FlatFileItemReader<Transaction> transactionReader() {
return new FlatFileItemReaderBuilder<Transaction>()
.name("transactionReader")
.resource(new FileSystemResource("transactions.csv"))
.delimited()
.names("id", "amount", "date")
.targetType(Transaction.class)
.build();
}
- CSV, Excel, 데이터베이스 등 다양한 소스에서 데이터 읽음
- 청크 단위로 처리 (예: 100개씩)
ItemProcessor: 데이터를 검증/변환하다
@Component
public class TransactionProcessor implements ItemProcessor<Transaction, Transaction> {
@Override
public Transaction process(Transaction transaction) throws Exception {
// 거래 금액 검증
if (transaction.getAmount() <= 0) {
throw new InvalidTransactionException("유효하지 않은 금액");
}
// 거래 상태 설정
transaction.setStatus("PROCESSED");
return transaction;
}
}
- 데이터 검증, 변환, 필터링 담당
- 검증 실패 시 예외 발생 → Listener에서 캐치
ItemWriter: 데이터를 저장하다
@Bean
public RepositoryItemWriter<Transaction> transactionWriter(TransactionRepository repository) {
RepositoryItemWriter<Transaction> writer = new RepositoryItemWriter<>();
writer.setRepository(repository);
writer.setMethodName("save");
return writer;
}
- 검증된 데이터를 데이터베이스에 저장
- 배치 단위로 저장 (예: 100개씩 한 번에)
CSV 파일에서
거래 100개 읽음"] --> B["✅ ItemProcessor
각 거래 검증"] B --> C{검증 결과?} C -->|성공| D["💾 ItemWriter
DB에 저장"] C -->|실패| E["🔔 Listener
오류 로그 테이블 저장"] style A fill:#1a202c,stroke:#90cdf4,stroke-width:2px,color:#e2e8f0 style B fill:#1a202c,stroke:#90cdf4,stroke-width:2px,color:#e2e8f0 style C fill:#2d3748,stroke:#f6ad55,stroke-width:2px,color:#e2e8f0 style D fill:#1a202c,stroke:#68d391,stroke-width:2px,color:#e2e8f0 style E fill:#1a202c,stroke:#fc8181,stroke-width:2px,color:#e2e8f0
3. 청크(Chunk): 메모리와 성능의 균형
여기서 중요한 개념이 청크(Chunk)입니다.
청크란?
한 번에 처리할 데이터의 단위입니다. 예를 들어, 청크 사이즈가 100이면:
1차: 1~100번 거래 읽기 → 검증 → 저장
2차: 101~200번 거래 읽기 → 검증 → 저장
3차: 201~300번 거래 읽기 → 검증 → 저장
...
왜 청크를 사용할까?
1개씩 처리:
거래 1: Read → Process → Write
거래 2: Read → Process → Write
...
거래 100만: Read → Process → Write
❌ 연산 오버헤드가 너무 큼
100~1000개씩 청크 처리:
청크 1: 1~100번 거래 동시 처리
청크 2: 101~200번 거래 동시 처리
...
✅ 성능과 메모리 균형
10,000개씩 처리:
한 번에 10,000개를 메모리에 로드
❌ 메모리 부하, 실패 시 재처리 범위 커짐
청크 사이즈 설정
@Bean
public Step transactionStep() {
return stepBuilderFactory.get("transactionStep")
.<Transaction, Transaction>chunk(1000) // 1000개씩 청크 처리
.reader(transactionReader())
.processor(transactionProcessor())
.writer(transactionWriter())
.build();
}
권장 청크 사이즈:
- 데이터 크기가 작으면 (예: 간단한 숫자) → 1000개 이상
- 데이터 크기가 크면 (예: 이미지, 복잡한 객체) → 100개 이하
- 실제 운영 환경에서 성능 테스트 후 결정
4. 에러 처리: Listener를 통한 우아한 실패 관리
100만 개의 거래 중 몇 개가 포맷 오류가 있다면?
일반적인 처리:
❌ 한 건 오류 발생 → 전체 Job 중단
Spring Batch의 처리:
✅ 한 건 오류 발생 → 로그 남기고 다음 거래 계속 처리
Listener 구현
@Component
public class TransactionProcessListener implements ItemProcessListener<Transaction, Transaction> {
private final ErrorLogRepository errorLogRepository;
@Override
public void onProcessError(Transaction item, Exception e) {
// 오류난 거래를 별도 테이블에 저장
ErrorLog errorLog = ErrorLog.builder()
.transactionId(item.getId())
.errorMessage(e.getMessage())
.errorTime(LocalDateTime.now())
.status("FAILED")
.build();
errorLogRepository.save(errorLog);
}
@Override
public void onProcessSuccess(Transaction item, Transaction result) {
// 성공한 거래에 대한 처리 (필요시)
}
}
Step에 Listener 등록
@Bean
public Step transactionStep() {
return stepBuilderFactory.get("transactionStep")
.<Transaction, Transaction>chunk(1000)
.reader(transactionReader())
.processor(transactionProcessor())
.writer(transactionWriter())
.listener(transactionProcessListener()) // Listener 등록
.build();
}
흐름 정리
거래 읽음"] --> B["✅ ItemProcessor
검증"] B --> C{검증
성공?} C -->|YES| D["💾 ItemWriter
정상 저장"] C -->|NO| E["🔔 Listener
onProcessError"] E --> F["📝 ErrorLog 테이블
오류 기록"] D --> G["다음 청크 처리"] F --> G style A fill:#1a202c,stroke:#90cdf4,stroke-width:2px,color:#e2e8f0 style B fill:#1a202c,stroke:#90cdf4,stroke-width:2px,color:#e2e8f0 style C fill:#2d3748,stroke:#f6ad55,stroke-width:2px,color:#e2e8f0 style D fill:#1a202c,stroke:#68d391,stroke-width:2px,color:#e2e8f0 style E fill:#1a202c,stroke:#fc8181,stroke-width:2px,color:#e2e8f0 style F fill:#1a202c,stroke:#fc8181,stroke-width:2px,color:#e2e8f0 style G fill:#2d3748,stroke:#90cdf4,stroke-width:2px,color:#e2e8f0
5. 실제 오류 로그 테이블 설계
오류를 효과적으로 추적하기 위한 테이블 구조:
CREATE TABLE error_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
transaction_id BIGINT NOT NULL,
error_message VARCHAR(500),
error_type VARCHAR(100),
error_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(50),
retry_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
각 컬럼의 의미:
transaction_id: 어떤 거래가 실패했는지error_message: 왜 실패했는지error_type: VALIDATION_ERROR, DB_ERROR 등 분류retry_count: 재처리 시도 횟수 추적status: FAILED, RETRYING, RESOLVED 등
이렇게 하면:
- 📊 통계: 처리된 거래 vs 실패한 거래 비교
- 🔄 재처리: 실패한 거래만 선택해서 다시 처리
- 📋 감사: 누가/언제/왜 실패했는지 기록
6. 전체 흐름 다시 보기
🎯 매일 새벽 2시 자동 실행 (Scheduler)
↓
📋 Job 시작: 거래 내역 일괄 처리
↓
📖 Step 1: 거래 데이터 검증 & 저장
├─ ItemReader: CSV에서 1000개씩 읽음
├─ ItemProcessor: 각 거래 검증
│ ├─ ✅ 성공 → ItemWriter에서 DB 저장
│ └─ ❌ 실패 → Listener에서 ErrorLog 테이블에 기록
↓
📊 Step 2: 통계 계산
├─ 전체 거래 수
├─ 성공한 거래 수
├─ 실패한 거래 수
↓
📄 Step 3: 최종 리포트 생성
├─ CSV로 내보내기
└─ 관리자에게 메일 발송
↓
✅ Job 완료
7. Spring Batch를 사용해야 하는 이유 정리
| 기능 | 일반 웹앱 | Spring Batch |
|---|---|---|
| 메모리 효율성 | 모든 데이터 메모리 로드 | 청크 단위로 처리 |
| 에러 처리 | 하나 실패 → 전체 실패 | 일부 실패해도 계속 진행 |
| 재시작 | 처음부터 다시 처리 | 마지막 지점부터 재개 |
| 모니터링 | 구현 필요 | 내장 JobRepository로 추적 |
| 스케줄링 | 추가 라이브러리 필요 | Spring Scheduler 통합 |
결론: 핵심 개념 요약
| 개념 | 설명 | 예시 |
|---|---|---|
| Job | 전체 작업 단위 | 거래 처리 전체 프로세스 |
| Step | 작업 내 개별 단계 | 검증&저장, 통계, 리포트 |
| ItemReader | 데이터 읽기 | CSV 파일에서 거래 읽음 |
| ItemProcessor | 데이터 검증/변환 | 거래 금액 유효성 검증 |
| ItemWriter | 데이터 저장 | DB에 거래 정보 저장 |
| Chunk | 한 번에 처리할 단위 | 1000개씩 묶어서 처리 |
| Listener | 이벤트 감시자 | 오류 발생 시 ErrorLog 저장 |
다음 단계
이제 기본 개념은 이해했습니다. 다음에 배워야 할 것들:
- ✅ 지금 배운 것: Job, Step, Reader/Processor/Writer, Chunk, Listener
- 🔜 다음에 배울 것: JobRepository, ExecutionContext, Scheduler 통합, 고급 에러 처리 (Skip, Retry)
여러분의 실무에서는 Spring Batch를 어떻게 활용하고 있나요? 거래 처리 외에 다른 사례가 있다면 댓글로 공유해주세요!
자신만의 철학을 만들어가는 중입니다.
댓글남기기