배치 프로세스 개념과 아키텍처
5. 자주 하는 실수 (Tip)
❌ 실수 1: Reader 가 전체를 반환한다
interface BadReader<T> {
List<T> readAll(); // 전체를 한 번에
}입력이 10만 건일 때는 동작하지만 1000만 건이 되는 날 OOM 으로 죽습니다. 배치는 "지금 데이터"가 아니라 "3년 뒤 데이터"를 기준으로 설계합니다. 그리고 readAll 은 첫 건을 처리하기 전에 마지막 건까지 다 읽어야 하므로 시작이 느립니다.
✅ 한 건씩 반환하고, 끝을 null 로 표시합니다.
interface ItemReader<T> {
T read() throws Exception; // 메모리 사용량이 입력 크기와 무관
}❌ 실수 2: Writer 가 한 건씩 받는다
interface BadWriter<T> {
void write(T item); // 건별 호출
}
// 100만 건 → INSERT 100만 번 → 네트워크 왕복 100만 번DB 배치 INSERT 는 건별 INSERT 보다 10~100배 빠릅니다. Writer 가 한 건씩 받으면 이 최적화를 할 자리가 없습니다.
✅ 묶음으로 받습니다. 한 건만 쓰고 싶으면 List.of(item) 을 넘기면 됩니다.
interface ItemWriter<T> {
void write(List<? extends T> items); // PreparedStatement.addBatch() 를 N번, executeBatch() 한 번
}❌ 실수 3: 예외가 나면 reader 를 안 닫는다
reader.open();
while ((item = reader.read()) != null) { ... } // 여기서 예외 발생
reader.close(); // 실행 안 됨 → 파일 핸들/DB 커서 누수배치는 오래 실행되고 여러 파일을 다룹니다. 핸들 누수가 쌓이면 "Too many open files" 로 다른 배치까지 죽습니다.
✅ finally 또는 try-with-resources 로 반드시 닫습니다. ItemReader 가 AutoCloseable 을 상속한 이유입니다.
try (ItemReader<T> r = createReader()) {
r.open();
...
} // 예외가 나도 close() 호출❌ 실수 4: 실행 결과를 로그에 한 줄만 남기고 끝낸다
System.out.println("batch done");아침에 "done" 만 보고 몇 건이 처리되었는지, 몇 분 걸렸는지, 어제와 비교해 정상인지 알 수 없습니다.
✅ 시작/종료 시각, 읽기/필터/쓰기/커밋 건수, 상태, 실패 메시지를 구조화된 형태(record → DB 또는 JSON 로그)로 남깁니다.
StepExecution exec = step.execute();
log.info(exec.summary()); // [regradeMembers] COMPLETED read=1000 ... 8ms
metadataRepository.save(exec); // 다음 날 추세 비교, 재시작 근거❌ 실수 5: Step 하나가 실패해도 다음 Step 을 실행한다
for (Step s : steps) s.execute(); // 실패 여부 무시"거래 집계" Step 이 실패했는데 "정산 파일 생성" Step 이 실행되면, 불완전한 데이터로 만든 정산 파일이 은행으로 전송됩니다.
✅ FAILED 면 즉시 중단하고 Job 상태를 FAILED 로 기록합니다. 이후 Step 이 "이전 Step 실패와 무관하게 실행되어야" 한다면 별도 Job 으로 분리합니다.