공공부하자개발 · 영어 학습 노트
자바
실무 배치대용량 데이터 처리0/6 완료
  • 01배치 프로세스 개념과 아키텍처
  • 02대용량 파일 I/O (NIO, Buffered)
  • 03Chunk 단위 처리와 OOM 방지
  • 04트랜잭션: 커밋과 롤백 시뮬레이션
  • 05Skip과 Retry 로직 구현
  • 06메서드 활용 패턴 (배치 유틸)
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 배치 › 01 / 6

배치 프로세스 개념과 아키텍처

섹션 7진행 0 / 6
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 온라인 트랜잭션 vs 배치

구분 온라인 트랜잭션(OLTP) 배치
트리거 사용자 요청(클릭, API 호출) 스케줄(매일 02:00), 파일 도착, 수동 실행
처리 단위 요청 1건 = 트랜잭션 1건 수십만~수억 건을 묶음으로
응답 시간 수십~수백 ms, 사람이 기다림 분~시간, 아무도 기다리지 않음
실패 시 사용자에게 오류 화면, 재시도는 사용자 몫 운영자가 새벽에 호출됨. 재시작 설계가 필수
자원 사용 짧고 잦음 길고 무거움. DB/디스크를 오래 점유
성능 관심사 지연 시간(latency) 처리량(throughput), 메모리
대표 코드 Controller → Service → Repository Reader → Processor → Writer

온라인 코드에서 "for 문으로 100만 번 DB 조회"는 상상하기 어렵지만, 배치에서는 그것이 본업입니다. 그래서 배치 코드는 같은 언어를 써도 설계 감각이 다릅니다. 온라인은 "이 한 건을 얼마나 빨리 끝내나", 배치는 "전체를 얼마나 안전하게, 메모리 안 터지고, 중간에 죽어도 이어서 끝내나"를 봅니다.

2.2 실무 배치 사례

사례 입력 처리 출력 특징
일마감 정산 당일 거래 내역 수백만 건 가맹점별 합산, 수수료 계산 정산 테이블, 은행 전송 파일 금액 정확성, 재실행 시 이중 정산 금지
대량 알림/메일 대상 회원 목록 템플릿 치환, 발송 API 호출 발송 결과 로그 외부 API 실패 → 재시도, 중복 발송 금지
데이터 마이그레이션 구 시스템 테이블 스키마 변환, 코드 매핑 신 시스템 테이블 수억 건, 검증 리포트 필수
리포트 생성 집계 대상 데이터 통계 계산 Excel/CSV/PDF 메모리 관리(전체를 한 번에 안 올리기)
정합성 검증 두 시스템의 잔액/재고 키 기준 비교 불일치 목록 대량 조인, 차이 발생 시 알림
회원 등급 재산정 회원 + 1년 구매 이력 등급 규칙 적용 회원 등급 UPDATE 매일 새벽, 전체 회원 대상
미납 알림 대상 추출 청구서 테이블 미납 & 기한 초과 필터 알림 큐 대부분 필터링되어 출력이 입력보다 훨씬 적음

공통점이 보입니다. 입력 → 가공 → 출력의 3단계이고, 입력이 매우 크고, 재실행이 안전해야 합니다.

2.3 배치의 핵심 요구사항

  1. 대용량: 입력이 메모리보다 클 수 있다는 전제로 설계합니다. List<Row> all = repository.findAll() 은 배치에서 금기어입니다.
  2. 재시작 가능성(restartability): 100만 건 중 70만 건에서 죽으면 70만 건부터 이어서 실행할 수 있어야 합니다. 그러려면 "어디까지 했는지"를 기록해야 합니다.
  3. 멱등성(idempotency): 같은 배치를 두 번 실행해도 결과가 한 번 실행한 것과 같아야 합니다. 정산을 두 번 하면 돈이 두 번 나갑니다.
  4. 로깅/모니터링: 시작·종료 시각, 읽은 건수, 쓴 건수, 실패 건수, 상태를 남겨야 새벽에 죽은 배치를 아침에 분석할 수 있습니다.
  5. 실패 격리: 한 건의 데이터 오류가 전체 배치를 죽이면 안 됩니다. 잘못된 건은 따로 기록하고 나머지는 계속 진행합니다(05 레슨).
  6. 성능: 실행 시간 창(window)이 있습니다. 새벽 2시~5시 안에 끝나야 아침 업무에 지장이 없습니다.

2.4 Reader → Processor → Writer — 왜 이렇게 나누는가

가장 단순한 배치 코드는 이렇습니다.

java
for (String line : Files.readAllLines(path)) {   // 읽기
    Member m = parse(line);
    m.setGrade(calc(m));                           // 가공
    db.update(m);                                  // 쓰기
}

이 코드에는 세 가지 문제가 있습니다.

  • readAllLines 가 파일 전체를 메모리에 올립니다. 100만 줄이면 수백 MB, 1000만 줄이면 OOM 입니다.
  • 한 건마다 db.update 를 호출합니다. DB 왕복이 100만 번입니다. 묶어서 보내면 100배 빨라집니다.
  • 읽기·가공·쓰기가 한 덩어리라 재사용도, 테스트도, 교체도 어렵습니다. 입력을 파일에서 DB 로 바꾸려면 전체를 고쳐야 합니다.

그래서 역할을 셋으로 나눕니다.

text
   ┌──────────────┐    1건     ┌──────────────┐    1건     ┌──────────────┐
   │  ItemReader  │ ────────▶ │ ItemProcessor│ ────────▶ │  (chunk 버퍼) │
   │  read(): T   │           │ process(I):O │           │  List<O>      │
   └──────────────┘           └──────────────┘           └──────┬───────┘
      파일/DB/API                 변환, 검증,                   │ N건 모이면
      한 건씩 꺼냄                필터(null)                    ▼
                                                        ┌──────────────┐
                                                        │  ItemWriter  │
                                                        │ write(List)  │
                                                        └──────────────┘
                                                          N건 한 번에 저장
                                                          = 커밋 포인트
역할 계약 왜 이 형태인가
ItemReader<T> T read() — 다음 한 건, 끝이면 null "다음 하나를 달라". 전체 크기와 무관하게 메모리가 일정
ItemProcessor<I,O> O process(I item) — null 이면 버림 입력·출력 타입이 달라도 됨. null 반환이 곧 필터
ItemWriter<T> void write(List<? extends T> items) 묶음을 받는다. batch insert·flush·bulk 호출이 효율적

Reader 는 "한 건씩", Writer 는 "묶음으로"라는 비대칭이 핵심입니다. 읽기는 메모리 때문에 한 건씩, 쓰기는 I/O 효율 때문에 묶음으로. 그 사이에서 묶음을 만드는 것이 청크(chunk)입니다.

2.5 Job / Step / Chunk — Spring Batch 용어와의 대응

text
Job (dailyMemberJob)
 ├── Step 1 (regradeMembers)      reader → processor → writer, chunk=100
 │     ├── chunk #1  (100건 읽고 → 가공 → 한 번에 쓰기 → 커밋)
 │     ├── chunk #2
 │     └── ...
 ├── Step 2 (extractUnpaid)       chunk=200
 └── Step 3 (sendNotifications)
개념 의미 이 레슨의 구현 Spring Batch
Job 하나의 배치 작업 전체. 이름과 실행 이력을 가짐 JobRunner Job, JobLauncher
Step Job 을 구성하는 독립 단계. 순서대로 실행, 하나 실패하면 중단 Step<I,O> Step, TaskletStep
Chunk Step 안에서 커밋 단위가 되는 N건 묶음 Step 의 chunkSize chunk(100)
ItemReader/Processor/Writer 위 2.4 동일 이름 인터페이스 동일 이름
StepExecution Step 한 번의 실행 통계 StepExecution record StepExecution + BATCH_STEP_EXECUTION 테이블
JobExecution Job 한 번의 실행 통계 JobRunner.JobExecution JobExecution + BATCH_JOB_EXECUTION 테이블

Spring Batch 는 여기에 메타데이터를 DB 테이블에 자동 저장하고, 재시작·스킵·재시도·병렬화를 설정으로 제공합니다. 하지만 구조 자체는 위 표 그대로입니다. 직접 만든 골격을 이해하면 Spring Batch 문서가 "우리가 만든 것의 확장판"으로 읽힙니다.

2.6 실행 메타데이터를 왜 기록하는가

배치는 사람이 보고 있지 않을 때 실행됩니다. 아침에 출근해서 확인할 수 있는 것은 오직 기록뿐입니다.

항목 없으면 생기는 일
시작/종료 시각 "어제보다 2시간 늦게 끝났는데 왜?" 에 답 못함. 실행 시간 추세를 볼 수 없어 언젠가 시간 창을 넘김
읽은 건수 / 쓴 건수 입력 100만, 출력 90만이면 10만 건이 필터인지 유실인지 모름
상태(STARTED/COMPLETED/FAILED) STARTED 로 남아 있으면 "아직 실행 중"인지 "죽었는데 기록 못 함"인지 구분. 이중 실행 방지에도 사용
종료 메시지(예외) 실패 원인을 로그 파일 수 GB 에서 찾아야 함
커밋 횟수 / 마지막 커밋 위치 재시작 불가. 처음부터 다시

StepExecution 을 record 로 만든 이유는 이것이 불변의 실행 기록이기 때문입니다. 실행이 끝난 뒤 값이 바뀌면 안 되고, toString/equals 가 자동으로 생기며, DB 한 행에 그대로 대응됩니다.

2.7 스케줄링과 실행 시간 창

배치는 누군가가 정해진 시각에 실행해 줘야 합니다.

방법 예 비고
Linux cron 0 2 * * * /opt/batch/run.sh daily 가장 흔함. 로그 리다이렉트와 실패 알림은 스크립트가 책임
Windows 작업 스케줄러 트리거: 매일 02:00, 동작: java -jar batch.jar daily GUI 설정, 이력은 이벤트 뷰어
애플리케이션 내 스케줄러 Spring @Scheduled, Quartz 서버 여러 대면 중복 실행 주의(분산 락 필요)
파일 도착 트리거 특정 폴더 감시(WatchService) 외부 기관 파일 수신 시
워크플로 도구 Airflow, Jenkins Job 간 의존관계, 재실행 UI

실행 시간 창(batch window)은 배치가 돌아도 되는 시간대입니다. 새벽 2시~5시라면 3시간 안에 끝나야 하고, 처리량이 하루 5% 씩 늘면 몇 달 뒤 창을 넘깁니다. 그래서 메타데이터에 소요 시간을 남기고 추세를 봐야 합니다. 또 같은 시각에 여러 배치가 몰리면 DB 가 느려지므로 순서와 의존관계를 설계합니다(정산 배치는 거래 마감 배치 이후에).

cron 표현식 한 줄만 기억합시다.

text
┌──────── 분 (0-59)
│ ┌────── 시 (0-23)
│ │ ┌──── 일 (1-31)
│ │ │ ┌── 월 (1-12)
│ │ │ │ ┌ 요일 (0-6, 0=일)
0 2 * * *      매일 02:00
0 3 1 * *      매월 1일 03:00
*/10 * * * *   10분마다

2.8 배치 설계 체크리스트

새 배치를 만들기 전에 답해야 할 질문입니다.

# 질문 관련 레슨
1 입력은 무엇이고 최대 몇 건인가? 메모리에 다 올릴 수 있는가? (거의 항상 "아니오"라고 가정) 02, 03
2 커밋 단위(청크 크기)는 얼마인가? 청크 하나가 실패하면 무엇이 롤백되는가? 03, 04
3 중간에 죽으면 어디서부터 재시작하는가? 그 위치를 어디에 기록하는가? 04
4 같은 입력으로 두 번 실행하면 결과가 같은가? (멱등성) 04
5 한 건의 데이터 오류에 전체가 죽어야 하는가, 건너뛰어야 하는가? 건너뛴 건은 어디에 남기는가? 05
6 외부 시스템(API, 타 DB) 호출이 있는가? 실패 시 재시도 정책은? 05
7 실행 시간 창은? 예상 소요 시간은? 병렬화가 필요한가? 03
8 시작/종료/건수/상태를 어디에 기록하고 누가 보는가? 실패 시 알림은? 01
9 이중 실행(어제 배치가 안 끝났는데 오늘 배치 시작)을 어떻게 막는가? 01, 04
10 입력 파일 인코딩, 날짜 형식, 구분자는 확정되었는가? 02
핵심 원리
  • 2.1 온라인 트랜잭션 vs 배치
  • 2.2 실무 배치 사례
  • 2.3 배치의 핵심 요구사항
  • 2.4 Reader → Processor → Writer — 왜 이렇게 나누는가
  • 2.5 Job / Step / Chunk — Spring Batch 용어와의 대응
  • 2.6 실행 메타데이터를 왜 기록하는가
  • 2.7 스케줄링과 실행 시간 창
  • 2.8 배치 설계 체크리스트
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제