본문 바로가기
Framework/Spring Boot

[Spring Boot] Spring Batch 입문: 1,000만 건의 데이터를 씹어 삼키다

반응형

웹 서비스 백엔드 개발자들은 주로 사용자가 스마트폰에서 버튼을 누르면 0.1초 만에 응답(JSON)을 주는 '실시간(Real-time)' 처리에 익숙합니다. 그런데 실무에서는 이런 요구사항이 비일비재하게 떨어집니다. "매월 1일 자정마다, 전체 회원 1,000만 명의 지난 한 달 치 결제 내역을 싹 다 조회해서 합산한 뒤, 50만 원 이상 쓴 사람들의 등급을 VIP로 업데이트하고 쿠폰을 발급해 주세요." 이 작업을 일반적인 스프링 웹 컨트롤러 안에서 for문 1,000만 번을 돌려서 처리하면 어떻게 될까요? 99.9% 확률로 메모리 초과(OOM)로 서버가 죽거나, 중간에 DB 커넥션이 끊겨서 '500만 명까지만 처리되고 멈추는' 끔찍한 대참사가 벌어집니다. 500만 명을 다시 어떻게 롤백할까요? 상상만 해도 끔찍합니다. 이 거대하고 위험한 대용량 일괄 처리(Batch Processing)를 아주 안전하고 우아하게, 끊겨도 다시 시작할 수 있게 만들어주는 프레임워크가 바로 Spring Batch(스프링 배치)입니다.

이번 [Spring Boot 실무 완벽 가이드] 시리즈에서는 "백그라운드 데이터 처리의 지배자!" Spring Batch의 웅장한 아키텍처와 도메인 언어에 대하여 다뤄보겠습니다.

⚙️ 1. Spring Batch의 핵심 개념: Job과 Step

스프링 배치는 거대한 작업을 아주 세밀하게 쪼개서 관리합니다. 이 단어들을 모르면 코드를 단 한 줄도 짤 수 없습니다.

Spring Batch 개념도
  • Job (작업): "월간 VIP 등급 업데이트 배치"라는 하나의 거대한 전체 프로젝트 단위입니다.
  • Step (단계): Job을 이루는 작은 블록들입니다. 예를 들어 Step 1: 휴면 회원 정리 -> Step 2: 결제액 합산 -> Step 3: VIP 등급 업데이트 처럼 여러 개의 Step을 조립해서 하나의 Job을 완성합니다.
  • JobInstance: "2023년 10월 1일 자정에 실행된 Job"처럼 특정 날짜/시간에 실행된 고유한 Job의 실체입니다. 만약 10월 1일 Job이 중간에 실패하면, 다음날 다시 실행할 때 새로운 Instance가 생기는 게 아니라 어제 실패했던 그 Instance의 실패한 지점부터 기가 막히게 이어서(Resume) 실행합니다! (이것이 배치를 쓰는 가장 큰 이유입니다)

🧱 2. Reader, Processor, Writer: 완벽한 분업화

Step 내부는 다시 3가지 역할로 완벽하게 분업(Chunk 지향 처리)되어 있습니다.

  • ItemReader: DB나 파일(CSV)에서 데이터를 '조금씩' 퍼 올립니다. (1,000만 건을 한 번에 메모리에 올리면 서버가 터지니까, 1,000건씩 쪼개서 가져옵니다.)
  • ItemProcessor: Reader가 퍼 올린 데이터를 '가공'합니다. (예: 결제액을 확인하고 VIP 여부를 true로 변경하는 비즈니스 로직) 필수는 아닙니다.
  • ItemWriter: Processor가 예쁘게 가공해 준 데이터를 모아두었다가 한 방에 DB에 UPDATE 쿼리를 쏩니다.

💻 3. 아주 심플한 Hello World Batch 코드

Spring Boot 3.x(Batch 5.x) 기준으로, 과거의 지저분했던 JobBuilderFactory가 사라지고 코드가 아주 깔끔해졌습니다.

@Configuration
@RequiredArgsConstructor
public class HelloWorldBatchConfig {

    private final JobRepository jobRepository; // 배치 작업의 성공/실패 기록을 DB에 저장하는 녀석
    private final PlatformTransactionManager transactionManager;

    // 1. 거대한 Job 만들기
    @Bean
    public Job helloJob() {
        return new JobBuilder("helloJob", jobRepository)
                .start(helloStep()) // helloStep부터 시작해!
                .build();
    }

    // 2. Job 안에서 실행될 Step 만들기 (여기서는 Reader/Writer 안 쓰고 단순 Tasklet(작은 일꾼) 사용)
    @Bean
    public Step helloStep() {
        return new StepBuilder("helloStep", jobRepository)
                .tasklet((contribution, chunkContext) -> {
                    System.out.println("===========================");
                    System.out.println("Hello Spring Batch!"); // 실제로는 여기에 무거운 로직이 들어감
                    System.out.println("===========================");
                    return RepeatStatus.FINISHED; // "나 일 다 끝났어!" 라고 보고
                }, transactionManager)
                .build();
    }
}

🎯 4. 마무리 및 다음 단계

지금까지 실시간 API 처리가 감당할 수 없는 엄청난 무게의 데이터를 분업화된 구조(Step, Reader, Writer)로 쪼개어 안전하게 처리하고, 중간에 실패하더라도 완벽하게 이어서(Resume) 실행할 수 있게 해주는 거대한 인프라, Spring Batch의 핵심 도메인 개념에 대해 상세히 다루어 보았습니다. 이제 대용량 데이터를 다루는 것에 대한 두려움이 사라졌습니다.

그런데 방금 본 예제는 너무 간단한 Tasklet(한 번에 다 처리해 버리는 방식)이었습니다. 실제 1,000만 건의 데이터를 처리할 때 Tasklet을 쓰면 여전히 OOM(Out of Memory) 에러를 피할 수 없습니다. 데이터를 메모리가 터지지 않게 '조금씩 쪼개서(Chunk)' 읽고, 가공하고, DB에 안전하게 밀어 넣는 진짜 실무 배치의 핵심 기술이 필요합니다. 이어지는 34단계 포스팅에서는 "수천만 건을 단 한 번의 메모리 터짐 없이 씹어 넘긴다!" Spring Batch의 꽃, Chunk(청크) 지향 처리 메커니즘과 PagingItemReader를 활용한 성능 최적화 비법에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!

반응형