티스토리 뷰

안녕하세요! 대량의 데이터를 효율적으로 처리해야 하는 문제에 직면해 본 경험이 있으신가요? 수백만, 수천만 건의 데이터를 일괄 업데이트하거나, 여러 파일에서 데이터를 읽어와 복잡한 비즈니스 로직을 거쳐 데이터베이스에 저장해야 할 때, 이 모든 과정을 수동으로 처리하는 것은 비효율적일 뿐만 아니라 오류 발생 가능성도 매우 높습니다. 이럴 때 필요한 것이 바로 배치(Batch) 처리이며, 자바 개발자들에게는 Spring Batch가 그 강력한 해답으로 자리매김하고 있습니다.

이 가이드는 프로그래밍 기초 지식이 있는 비전공자부터 Spring/Java 개발자까지 누구나 Spring Boot Batch를 이해하고 데이터 처리 자동화의 효과를 경험할 수 있도록 돕는 실용적인 가이드입니다. 우리는 스프링 배치 란 무엇인지부터 시작하여, 실제 spring boot batch 예제를 통해 첫 번째 배치 잡을 만들고, 나아가 spring boot batch 대용량 처리스프링 배치 성능 최적화 팁까지 함께 탐구할 것입니다. 마치 복잡한 레시피를 따라 요리를 하듯, 차근차근 각 단계를 밟아가며 데이터 처리 자동화의 핵심을 이해하게 될 것입니다.

이 가이드를 통해 여러분은 대용량 데이터 처리의 어려움을 극복하고, 반복적인 작업을 자동화하여 개발 생산성을 효과적으로 향상시킬 수 있는 통찰력을 얻게 될 것입니다. 그럼, 지금부터 Spring Boot Batch의 세계로 함께 떠나볼까요?


1. Spring Batch, 왜 필요하며 무엇을 제공할까요?

대용량 데이터 처리, 왜 어려울까요?

데이터는 현대 사회의 원유라고 불릴 만큼 중요하지만, 그만큼 다루기 어렵습니다. 특히, 수백만 건 이상의 대용량 데이터를 처리해야 할 때는 더욱 그렇습니다. 매일 밤 수많은 고객의 결제 내역을 정산하거나, 방대한 양의 사용자 로그를 분석하여 리포트를 생성하고, 혹은 특정 기간 동안 쌓인 데이터를 한 번에 업데이트해야 하는 상황을 상상해 보세요.

이러한 작업들은 다음과 같은 특징과 어려움을 가집니다:

  • 데이터의 방대함: 처리할 데이터가 너무 많아 한 번에 메모리에 올리거나 실시간으로 처리하기 어렵습니다.
  • 반복성: 매일, 매주, 매월 같은 작업을 반복해야 합니다. 이 반복 작업을 수동으로 하거나, 단순히 스크립트 하나로 돌리기에는 관리 부담이 큽니다.
  • 안정성 요구: 중간에 에러가 발생하면 처음부터 다시 시작해야 할까요? 아니면 실패한 지점부터 이어서 처리할 수 있을까요? 데이터의 정합성을 유지하면서 안정적으로 완료하는 것이 중요합니다.
  • 트랜잭션 관리의 복잡성: 수많은 데이터 중 일부만 처리되고 실패한다면, 데이터가 엉망이 될 수 있습니다. 전체 작업 단위를 정확하게 관리해야 합니다.
  • 예측 불가능성: 처리 시간에 영향을 미치는 요소가 많고, 자원 소모가 크기 때문에 예측과 관리가 어렵습니다.

이러한 문제들로 인해 개발자들은 대용량 데이터 처리에 많은 시간과 노력을 쏟게 되고, 이는 곧 개발 지연과 서비스 안정성 저하로 이어질 수 있습니다.

Spring Batch의 등장 배경과 핵심 역할

이러한 문제들을 해결하기 위해 탄생한 것이 바로 Spring Batch입니다. Spring Batch는 엔터프라이즈 시스템에서 대규모 배치 작업을 효율적으로 수행하도록 설계된 경량의 포괄적인 배치 프레임워크입니다. 즉, "대용량 데이터를 안정적이고 효율적으로 처리하기 위한 스프링 기반의 도구 상자"라고 생각하시면 됩니다.

비전공자분들을 위한 쉬운 비유를 들어볼까요? Spring Batch는 마치 공장의 자동화된 생산 라인과 같습니다.

  • 재료 공급 (데이터 읽기): 생산 라인의 시작에서 원재료(데이터)를 가져옵니다.
  • 가공 및 조립 (데이터 처리): 가져온 재료를 정해진 규칙에 따라 가공하고 조립합니다. 불량품(오류 데이터)은 걸러내거나 다시 처리합니다.
  • 제품 포장 및 출하 (데이터 쓰기): 완성된 제품(처리된 데이터)을 최종 목적지(데이터베이스, 파일 등)로 보냅니다.
  • 생산 라인 관리자 (Spring Batch 프레임워크): 이 모든 과정을 자동화하고, 중간에 문제가 생기면 어디서 문제가 발생했는지 파악하여 해당 지점부터 다시 시작할 수 있도록 돕습니다.

Spring Batch는 이러한 자동화된 생산 라인을 구축하는 데 필요한 모든 구성 요소와 기능을 제공하여, 개발자들이 비즈니스 로직에만 집중할 수 있도록 돕습니다.

Spring Batch의 주요 장점

Spring Batch를 사용하면 얻을 수 있는 핵심적인 장점들은 다음과 같습니다:

  1. 자동화 (Automation):
    • 반복 작업의 해방: 매일, 매주 반복되는 작업을 수동으로 실행하거나 복잡한 스크립트를 관리할 필요 없이, 한 번 구축해두면 정해진 스케줄에 따라 자동으로 실행됩니다. 예를 들어, 매일 새벽 3시에 전날 매출 데이터를 정산하는 작업을 Spring Batch로 구현하면, 개발자가 직접 신경 쓰지 않아도 됩니다.
    • 코드의 재사용성: 배치 작업에서 자주 사용되는 패턴(파일 읽기, DB 쓰기 등)을 미리 구현해놓았기 때문에, 개발자는 이를 조합하여 손쉽게 새로운 배치 작업을 만들 수 있습니다. 이는 개발 시간을 단축하고 코드의 일관성을 유지하는 데 큰 도움이 됩니다.
  2. 안정성 (Stability) 및 견고함:
    • 재시작 가능성 (Restartability): 배치 작업은 수십만 건의 데이터를 처리하다가 네트워크 오류, 서버 다운 등 예기치 않은 문제로 중간에 중단될 수 있습니다. Spring Batch는 이러한 상황에 대비하여 작업의 현재 상태를 기록하고, 실패 시 실패한 지점부터 다시 시작할 수 있도록 지원합니다. 이는 처음부터 다시 시작하는 비효율과 데이터 중복/누락의 위험을 방지해 줍니다. 예를 들어, 100만 건 중 50만 건 처리 후 실패했다면, 다음 실행 시 50만 1번째 데이터부터 처리합니다.
    • 트랜잭션 관리 (Transaction Management): 대량의 데이터를 한 번에 처리하는 것이 아니라, 작은 단위(청크)로 쪼개어 처리하고 각 청크마다 트랜잭션을 적용합니다. 만약 한 청크 내에서 오류가 발생하면, 해당 청크의 작업만 롤백되고 다음 청크로 넘어갈 수 있습니다. 이는 데이터의 정합성을 보장하며, 전체 작업 실패로 인한 손실을 최소화합니다.
    • 에러 처리 및 스킵: 특정 데이터에 문제가 있어도 전체 작업이 멈추는 것을 방지하기 위해, 문제가 있는 데이터를 건너뛰거나(Skip), 특정 횟수만큼 재시도(Retry)하는 기능을 제공합니다.
  3. 확장성 (Scalability):
    • 대용량 처리 지원: Spring Batch는 ItemReader, ItemProcessor, ItemWriter 등의 구성 요소를 통해 대용량 데이터를 효율적으로 읽고, 처리하고, 쓸 수 있도록 설계되어 있습니다. 데이터의 양이 늘어나더라도 안정적으로 처리할 수 있는 기반을 제공합니다.
    • 다양한 데이터 소스 지원: 파일(CSV, TXT, XML), 데이터베이스(관계형 DB, NoSQL), 메시지 큐 등 다양한 형태의 데이터 소스를 읽고 쓸 수 있는 유연성을 제공합니다.
    • 성능 최적화 옵션: 멀티스레딩, 원격 청킹(Remote Chunking), 파티셔닝(Partitioning)과 같은 고급 기능을 통해 배치 작업의 성능을 극대화할 수 있는 방법을 제공합니다. 이는 데이터 볼륨이 기하급수적으로 증가하더라도 효과적으로 대응할 수 있게 합니다.

결론적으로, Spring Batch는 반복적이고 대용량인 데이터 처리 작업을 자동화하고, 안정적으로 관리하며, 미래의 데이터 증가에도 유연하게 대응할 수 있도록 돕는 강력한 도구입니다. 이 프레임워크를 이해하고 활용하는 것은 현대 소프트웨어 개발에서 매우 중요한 경쟁력이 될 것입니다. 이제 다음 섹션에서는 Spring Batch의 이러한 장점들을 가능하게 하는 핵심 구성 요소들을 더 깊이 있게 살펴보겠습니다.


2. Spring Batch 핵심 개념: Job, Step, ItemReader, ItemProcessor, ItemWriter

Spring Batch의 강력함은 잘 정의된 핵심 개념들의 유기적인 결합에서 나옵니다. 이 섹션에서는 Spring Batch의 기본 빌딩 블록인 Job, Step, ItemReader, ItemProcessor, ItemWriter의 역할과 상호작용을 쉽고 명확하게 설명하고, 비유와 함께 그 구조를 파악해 보겠습니다.

핵심 개념 이해를 위한 비유: 빵 굽기 프로젝트

Spring Batch의 주요 구성 요소를 이해하기 위해 '빵 굽기 프로젝트'에 비유해 봅시다.

  • Job (작업): "특정 빵을 굽는 전체 프로젝트"입니다. 예를 들어, '호밀빵 굽기 프로젝트'나 '크루아상 만들기 프로젝트'처럼, 하나의 큰 목표를 가진 작업 단위입니다.
  • Step (단계): Job을 구성하는 개별적인 "작업 단계"입니다. 호밀빵 굽기 프로젝트는 '재료 준비', '반죽하기', '발효하기', '굽기', '포장하기' 등의 여러 Step으로 나눌 수 있습니다. 각 Step은 독립적인 작업 단위입니다.
  • ItemReader (아이템 리더): 각 Step에서 "빵의 재료를 가져오는 역할"을 합니다. '밀가루 창고'에서 밀가루를 가져오거나, '냉장고'에서 우유와 계란을 가져오는 것과 같습니다. 데이터 소스(파일, DB)에서 데이터를 한 건씩 읽어옵니다.
  • ItemProcessor (아이템 프로세서): 읽어온 "재료를 가공하는 역할"을 합니다. 가져온 밀가루를 체에 치거나, 계란을 잘 풀어주는 것과 같습니다. 읽어온 데이터에 비즈니스 로직을 적용하거나, 형식을 변환합니다.
  • ItemWriter (아이템 라이터): 가공이 끝난 "재료를 모아 최종적으로 빵을 만들거나 포장하는 역할"을 합니다. 반죽을 오븐에 넣거나, 구워진 빵을 포장지에 담는 것과 같습니다. 처리된 데이터를 최종 목적지(DB, 파일)에 저장합니다.

이제 이 비유를 바탕으로 각 구성 요소를 좀 더 자세히 살펴보겠습니다.

1. Job (잡)

Job은 Spring Batch에서 가장 큰 단위의 배치 작업을 나타냅니다. 예를 들어, "월말 정산 배치", "사용자 데이터 이관 배치"와 같이 하나의 비즈니스 목표를 가진 전체 작업을 의미합니다.

  • 역할: 하나 이상의 Step으로 구성되며, JobParameters를 통해 외부로부터 실행에 필요한 인자(예: 실행 일자, 파일 경로)를 전달받을 수 있습니다.
  • 핵심 구성:
    • JobExecution: 각 Job이 실행될 때마다 생성되는 인스턴스로, Job의 실행 상태, 시작/종료 시간, 결과 등을 기록합니다.
    • JobInstance: JobParameters가 동일한 Job 실행들을 묶는 논리적인 실행 단위입니다.
    • JobLauncher: Job을 실행하는 인터페이스입니다.

2. Step (스텝)

StepJob 내의 독립적이고 순차적인 단일 작업 단계입니다. 하나의 Job은 여러 Step으로 구성될 수 있으며, 각 Step은 특정 순서에 따라 실행됩니다. 빵 굽기 비유에서 '재료 준비', '반죽하기', '굽기' 각각이 Step에 해당합니다.

Spring Batch의 Step은 크게 두 가지 유형으로 나뉩니다:

  • Tasklet Step: 하나의 단일 작업을 수행하는 가장 기본적인 Step입니다. 특정 파일을 삭제하거나, 특정 테이블의 데이터를 한 번에 비우는(truncate) 등, ItemReader, ItemProcessor, ItemWriter의 패턴에 맞지 않는 간단한 작업에 주로 사용됩니다.
  • Chunk-oriented Step (청크 기반 스텝): 대용량 데이터 처리의 핵심이자 Spring Batch의 강력함을 보여주는 주요 유형입니다. 데이터를 "읽고(Read) → 처리하고(Process) → 쓰는(Write)" 과정을 반복하며, 이 일련의 작업을 청크(Chunk) 단위로 묶어 처리합니다. 대부분의 배치 작업은 이 청크 기반 스텝을 사용합니다.

3. 청크(Chunk) 기반 처리의 이해: Read-Process-Write

청크 기반 스텝은 ItemReader, ItemProcessor, ItemWriter 세 가지 핵심 컴포넌트의 협업으로 이루어집니다. "청크"란 한 번의 트랜잭션 내에서 처리되는 데이터의 묶음을 의미합니다.

Spring Batch의 핵심 구성 요소인 Job, Step, ItemReader, ItemProcessor, ItemWriter의 유기적인 데이터 처리 흐름을 보여주는 다이어그램. 데이터가 각 단계를 거치며 가공되고 최종 목적지로 전달되는 효율적인 과정

  • ItemReader (아이템 리더):
    • 역할: 데이터 소스(파일, DB 등)에서 데이터를 한 건(Item)씩 읽어오는 역할을 합니다.
    • 특징: read() 메서드를 호출할 때마다 다음 Item을 반환하며, 더 이상 읽을 Item이 없으면 null을 반환하여 읽기 종료를 알립니다. Cursor 기반 또는 Paging 기반 Reader, FlatFileItemReader 등이 주로 사용됩니다.
    • 예시: CSV 파일에서 한 줄씩 고객 정보를 읽어오기, 데이터베이스에서 사용자 목록을 한 행씩 가져오기.
  • ItemProcessor (아이템 프로세서):
    • 역할: ItemReader가 읽어온 데이터를 비즈니스 로직에 따라 가공하거나 필터링하는 역할을 합니다.
    • 특징: process() 메서드를 구현하여 ItemReader에서 받은 Item을 변형하거나, 특정 조건에 따라 null을 반환하여 해당 Item을 다음 단계(ItemWriter)로 넘기지 않고 필터링할 수 있습니다. 선택 사항이며, 모든 배치 작업에 필수로 필요한 것은 아닙니다.
    • 예시: 읽어온 고객 정보에서 '이름' 필드의 앞뒤 공백 제거, '나이' 필드가 20세 미만이면 필터링, 상품 가격 10% 인상.
  • ItemWriter (아이템 라이터):
    • 역할: ItemProcessor (또는 ItemReader)로부터 전달받은 가공된 데이터 Item들의 묶음(Chunk)을 최종 목적지에 기록하는 역할을 합니다.
    • 특징: write(List<? extends T> items) 메서드를 통해 한 번에 여러 Item(청크 단위)을 받아서 처리합니다. 이때, 이 write 메서드 호출 자체가 하나의 트랜잭션으로 묶여 처리됩니다. 따라서, 청크 내에서 하나의 Item이라도 쓰기 작업에 실패하면 해당 청크 전체가 롤백됩니다. JdbcBatchItemWriter, JpaItemWriter, FlatFileItemWriter 등이 있습니다.
    • 예시: 처리된 고객 정보를 데이터베이스 테이블에 일괄 삽입, 가공된 상품 데이터를 새로운 CSV 파일에 쓰기.

청크 기반 처리의 동작 방식

  1. ItemReader는 설정된 chunk size만큼의 Item을 하나씩 읽어옵니다.
  2. 읽어온 각 Item은 ItemProcessor로 전달되어 개별적으로 처리됩니다. (옵션)
  3. chunk size만큼 Item이 모두 읽혀지고 처리되면, 이 Item들의 리스트(청크)가 ItemWriter로 전달됩니다.
  4. ItemWriter는 전달받은 청크 전체를 한 번의 트랜잭션으로 최종 목적지에 기록합니다.
  5. 이 과정(Read -> Process -> Write)이 반복되어 모든 데이터가 처리될 때까지 계속됩니다.

이러한 청크 기반 처리는 대용량 데이터를 처리할 때 메모리 사용량을 효율적으로 관리하고, 트랜잭션 단위를 명확하게 하여 안정성을 높이는 데 핵심적인 역할을 합니다. 데이터의 양이 아무리 많아도, 메모리에는 항상 chunk size만큼의 데이터만 올라와 처리되므로 OutOfMemoryError와 같은 문제를 방지할 수 있습니다.

결론적으로, Job은 전체 프로젝트, Step은 그 프로젝트의 개별 작업 단계, 그리고 ItemReader, ItemProcessor, ItemWriter는 그 단계 안에서 데이터를 읽고, 가공하고, 쓰는 역할을 분담하는 중요한 구성 요소들입니다. 이들의 유기적인 협업이 바로 Spring Batch가 대용량 데이터 처리를 안정적이고 효율적으로 수행할 수 있게 하는 비결입니다. 이제 이 개념들을 바탕으로 실제 Spring Boot Batch 프로젝트를 시작해 보겠습니다.


3. Spring Boot Batch 프로젝트 시작하기: 기본 환경 설정 및 첫 번째 배치 잡 생성

이 섹션에서는 Spring Boot 환경에서 Spring Batch 프로젝트를 설정하고, 간단한 배치 잡(Job)을 생성하여 실행하는 방법을 단계별로 설명합니다. 우리는 "CSV 파일에서 데이터를 읽어와 콘솔에 출력하는" 가장 기본적인 예제를 통해 ItemReaderItemWriter, 그리고 Job, Step의 구성을 직접 경험해 볼 것입니다. 이를 통해 spring boot batch 설정의 기본을 다지고, 첫 spring boot batch 예제를 성공적으로 만들어봅시다.

3.1. Spring Boot Batch 기본 환경 설정

가장 먼저 Spring Boot 프로젝트를 생성하고, Spring Batch 관련 의존성을 추가해야 합니다.

1. 프로젝트 생성 (Spring Initializr 사용 권장)
Spring Initializr에서 다음 옵션으로 프로젝트를 생성합니다.

  • Project: Maven Project (또는 Gradle Project)
  • Language: Java
  • Spring Boot: 최신 안정 버전 (예: 3.2.x)
  • Dependencies: Spring Batch, H2 Database, Lombok, Spring Boot DevTools

2. pom.xml (Maven) 의존성 확인
생성된 프로젝트의 pom.xml 파일을 열어 다음 의존성이 포함되어 있는지 확인합니다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version> <!-- 최신 안정 버전으로 설정 -->
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-batch-hello-world</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-batch-hello-world</name>
    <description>Demo project for Spring Boot Batch</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.batch</groupId>
            <artifactId>spring-batch-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

3. application.properties 설정
Spring Batch는 Job의 실행 메타데이터(어떤 Job이 언제 시작했고, 성공했는지, 실패했다면 어디까지 진행됐는지 등)를 저장하기 위한 데이터베이스가 필요합니다. H2 Database를 인메모리 방식으로 사용하여 간편하게 설정할 수 있습니다.

src/main/resources/application.properties 파일에 다음 내용을 추가합니다.

# H2 Database 설정 (Batch 메타데이터 저장용)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# Spring Batch 설정
# Batch 메타데이터 테이블 생성 여부. 개발 단계에서는 true, 운영 시에는 false (직접 스키마 관리)
spring.batch.initialize-schema=always
# Spring Boot 애플리케이션 시작 시 자동으로 모든 Job을 실행할지 여부.
# 개발 시에는 수동 실행을 위해 false로 설정하는 경우가 많습니다.
spring.batch.job.enabled=false

spring.batch.job.enabled=false로 설정하는 이유는, Spring Boot 애플리케이션 시작 시 모든 Job 빈(Bean)이 자동으로 실행되는 것을 방지하기 위함입니다. 우리는 JobRunner를 통해 원하는 Job을 수동으로 실행할 것입니다.

3.2. 첫 번째 배치 잡 생성: CSV 파일 읽어 콘솔 출력

이제 실제 배치 잡을 만들어 봅시다. 목표는 다음과 같습니다:

  1. input.csv 파일 생성
  2. input.csv에서 데이터를 한 줄씩 읽어오기 (ItemReader)
  3. 읽어온 데이터를 가공 없이 콘솔에 출력하기 (ItemWriter)
  4. 이 과정을 Step으로 묶고, 최종적으로 Job으로 정의하여 실행하기

1. 처리할 데이터 모델 정의 (Person.java)

CSV 파일의 각 행을 매핑할 자바 객체를 생성합니다.
src/main/java/com/example/springbatchhelloworld/Person.java

package com.example.springbatchhelloworld;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@NoArgsConstructor
@ToString
public class Person {
    private String firstName;
    private String lastName;
    private String email;

    public Person(String firstName, String lastName, String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }
}

2. 입력 CSV 파일 생성 (input.csv)

src/main/resources/input.csv 파일을 생성하고 다음 내용을 추가합니다.

John,Doe,john.doe@example.com
Jane,Smith,jane.smith@example.com
Peter,Jones,peter.jones@example.com
Alice,Williams,alice.williams@example.com

3. 배치 설정 클래스 (BatchConfig.java)

이제 Job, Step, ItemReader, ItemWriter를 정의하는 배치 설정 클래스를 작성합니다. @EnableBatchProcessing 어노테이션을 붙여 Spring Batch 기능을 활성화합니다.

src/main/java/com/example/springbatchhelloworld/BatchConfig.java

package com.example.springbatchhelloworld;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
// Spring Batch 기능 활성화. Spring Boot 3.x 부터는 Spring Batch 5.x를 사용하며,
// JobBuilderFactory/StepBuilderFactory는 Deprecated 되었으므로 직접 JobBuilder/StepBuilder를 주입받아 사용합니다.
public class BatchConfig {

    // JobRepository: 배치 메타데이터를 저장하고 관리하는 핵심 컴포넌트
    private final JobRepository jobRepository;
    // PlatformTransactionManager: 배치 작업의 트랜잭션을 관리
    private final PlatformTransactionManager transactionManager;

    public BatchConfig(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        this.jobRepository = jobRepository;
        this.transactionManager = transactionManager;
    }

    // ItemReader 정의: CSV 파일에서 Person 객체를 읽어오는 역할
    @Bean
    public FlatFileItemReader<Person> personItemReader() {
        return new FlatFileItemReaderBuilder<Person>()
                .name("personItemReader") // Reader의 고유 이름
                .resource(new ClassPathResource("input.csv")) // 읽어올 CSV 파일 경로
                .delimited() // 구분자로 필드를 분리
                .names("firstName", "lastName", "email") // CSV 파일의 각 열에 대한 논리적인 이름을 지정 (Person 객체의 필드 이름과 매핑)
                .lineMapper(new DefaultLineMapper<>() {{ // 각 라인을 Person 객체로 매핑
                    setLineTokenizer(new DelimitedLineTokenizer() {{ // 라인을 토큰으로 분리하는 토크나이저
                        setNames("firstName", "lastName", "email"); // 토큰 이름 설정
                    }});
                    setFieldSetMapper(new BeanWrapperFieldSetMapper<>() {{ // 토큰을 Person 객체의 필드에 매핑
                        setTargetType(Person.class); // 매핑할 대상 클래스
                    }});
                }})
                .build();
    }

    // ItemWriter 정의: Person 객체를 콘솔에 출력하는 역할
    @Bean
    public ItemWriter<Person> personItemWriter() {
        return items -> { // 람다식을 사용하여 ItemWriter 구현 (items는 청크 단위의 Person 리스트)
            for (Person person : items) {
                System.out.println("읽어온 데이터: " + person);
            }
        };
    }

    // Step 정의: ItemReader로 읽고, ItemWriter로 쓰는 하나의 배치 단계
    @Bean
    public Step processPersonStep() {
        return new StepBuilder("processPersonStep", jobRepository) // Step의 이름과 JobRepository 주입
                .<Person, Person>chunk(3, transactionManager) // 청크 단위 설정: 3개씩 읽고 처리, 트랜잭션 매니저 주입
                .reader(personItemReader()) // ItemReader 연결
                // .processor(personItemProcessor()) // ItemProcessor는 선택 사항이므로 여기서는 생략
                .writer(personItemWriter()) // ItemWriter 연결
                .build();
    }

    // Job 정의: 전체 배치 작업을 구성하는 Job
    @Bean
    public Job importUserJob() {
        return new JobBuilder("importUserJob", jobRepository) // Job의 이름과 JobRepository 주입
                .incrementer(new RunIdIncrementer()) // JobParameters를 매번 다르게 생성하여 JobInstance가 매번 새로 생성되도록 함
                .start(processPersonStep()) // Job의 첫 번째 Step으로 processPersonStep 설정
                .build();
    }
}

코드 설명:

  • @Configuration: 이 클래스가 Spring의 설정 클래스임을 나타냅니다.
  • JobRepository, PlatformTransactionManager: Spring Batch의 핵심 빈들로, 생성자를 통해 주입받습니다. Spring Boot 3.x에서는 JobBuilderFactoryStepBuilderFactory가 Deprecated 되었으므로 JobBuilderStepBuilder를 직접 사용하여 JobRepositoryPlatformTransactionManager를 주입해야 합니다.
  • personItemReader(): FlatFileItemReaderBuilder를 사용하여 CSV 파일을 읽는 ItemReader를 구성합니다.
    • resource(new ClassPathResource("input.csv")): src/main/resources 폴더에 있는 input.csv 파일을 읽도록 지정합니다.
    • names("firstName", "lastName", "email"): CSV 파일의 각 열을 Person 객체의 해당 필드에 매핑합니다.
  • personItemWriter(): 람다 표현식을 사용하여 ItemWriter를 구현했습니다. chunk(3)에서 설정한 대로 3개의 Person 객체 리스트를 받으며, 각 객체를 콘솔에 출력합니다.
  • processPersonStep(): StepBuilder를 사용하여 하나의 Step을 정의합니다.
    • chunk(3, transactionManager): 이 Step이 청크 기반으로 동작하며, 한 번에 3개의 아이템을 처리할 것을 명시합니다. 이 3개의 아이템 처리 과정이 하나의 트랜잭션으로 묶입니다.
    • reader(personItemReader()), writer(personItemWriter()): 위에서 정의한 ItemReaderItemWriter를 이 Step에 연결합니다.
  • importUserJob(): JobBuilder를 사용하여 최종 Job을 정의합니다.
    • incrementer(new RunIdIncrementer()): JobParametersrun.id라는 파라미터를 자동으로 추가하여 매번 Job을 실행할 때마다 새로운 JobInstance가 생성되도록 합니다. 이는 개발 시 여러 번 테스트하기 편리합니다.
    • start(processPersonStep()): importUserJob이 시작되면 processPersonStep을 가장 먼저 실행하도록 지정합니다.

3.3. Job 실행기 (Runner) 생성

application.properties에서 spring.batch.job.enabled=false로 설정했기 때문에, 애플리케이션 시작 시 Job이 자동으로 실행되지 않습니다. 따라서 별도의 컴포넌트를 만들어 Job을 수동으로 실행해야 합니다.

src/main/java/com/example/springbatchhelloworld/JobRunner.java

package com.example.springbatchhelloworld;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class JobRunner implements CommandLineRunner {

    private final JobLauncher jobLauncher;
    private final Job importUserJob; // BatchConfig에서 정의한 Job 빈을 주입받습니다.

    public JobRunner(JobLauncher jobLauncher, Job importUserJob) {
        this.jobLauncher = jobLauncher;
        this.importUserJob = importUserJob;
    }

    @Override
    public void run(String... args) throws Exception {
        // JobParameters를 생성합니다. JobParameters는 Job 실행에 필요한 인자를 전달합니다.
        // 현재 시간(System.currentTimeMillis())을 사용하여 JobParameters를 매번 다르게 만들어,
        // Spring Batch가 새로운 JobInstance로 인식하고 실행하도록 합니다.
        JobParameters jobParameters = new JobParametersBuilder()
                .addLong("time", System.currentTimeMillis())
                .toJobParameters();

        try {
            System.out.println("------------------------------------");
            System.out.println("Spring Batch Job 실행 시작: " + importUserJob.getName());
            jobLauncher.run(importUserJob, jobParameters);
            System.out.println("Spring Batch Job 실행 완료!");
            System.out.println("------------------------------------");
        } catch (Exception e) {
            System.err.println("Spring Batch Job 실행 중 오류 발생: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

코드 설명:

  • @Component: 이 클래스를 Spring 빈으로 등록합니다.
  • CommandLineRunner: Spring Boot 애플리케이션이 시작된 후 run 메서드가 자동으로 실행되도록 하는 인터페이스입니다.
  • JobLauncher: Spring Batch의 Job을 실행하는 핵심 인터페이스입니다.
  • Job importUserJob: BatchConfig에서 @Bean으로 등록한 importUserJob 빈을 주입받습니다.
  • JobParametersBuilder: JobParameters를 생성하는 빌더입니다. addLong("time", System.currentTimeMillis())를 통해 고유한 JobParameters를 생성함으로써, 매번 새로운 JobInstance를 만들고 Job을 실행할 수 있습니다. 이는 테스트나 여러 번 실행할 때 중요합니다.

3.4. 애플리케이션 실행 및 결과 확인

이제 모든 설정이 완료되었습니다. SpringBatchHelloWorldApplication.java (main 클래스)를 실행하거나, Maven mvn spring-boot:run 또는 Gradle gradle bootRun 명령어를 통해 애플리케이션을 실행합니다.

예상되는 콘솔 출력:

------------------------------------
Spring Batch Job 실행 시작: importUserJob
------------------------------------
읽어온 데이터: Person(firstName=John, lastName=Doe, email=john.doe@example.com)
읽어온 데이터: Person(firstName=Jane, lastName=Smith, email=jane.smith@example.com)
읽어온 데이터: Person(firstName=Peter, lastName=Jones, email=peter.jones@example.com)
읽어온 데이터: Person(firstName=Alice, lastName=Williams, email=alice.williams@example.com)
Spring Batch Job 실행 완료!
------------------------------------

이 출력은 input.csv 파일의 모든 라인이 성공적으로 읽혀 Person 객체로 매핑된 후 콘솔에 출력되었음을 보여줍니다.

이것으로 Spring Boot Batch 환경 설정과 첫 번째 배치 잡 생성이 완료되었습니다! 이제 여러분은 Job, Step, ItemReader, ItemProcessor, ItemWriter가 어떻게 결합되어 하나의 배치 작업을 구성하는지 기본적인 흐름을 이해하셨을 것입니다. 다음 섹션에서는 이 기본 지식을 바탕으로 더욱 복잡하고 실용적인 예제를 통해 Spring Batch의 진정한 위력을 경험해 보겠습니다.


4. 실전 예제로 배우는 Spring Batch: CSV 파일 읽어 DB에 저장 (CSV to DB)

이 섹션에서는 앞서 배운 Spring Batch의 핵심 개념과 기본 환경 설정을 바탕으로, 실제 시나리오에서 자주 접하는 대용량 데이터 처리 예제를 구현해 보겠습니다. 목표는 CSV 파일을 읽어 데이터를 가공(Process)한 후 데이터베이스에 저장하는 실용적인 배치 잡을 만드는 것입니다. 이 과정을 통해 ItemReader, ItemProcessor, ItemWriter의 구현 방법을 상세히 다루고, 기본적인 에러 처리 및 트랜잭션 관리 개념도 함께 살펴보겠습니다.

이 예제는 spring batch csv to db 시나리오를 충실히 따르며, spring boot batch 대용량 처리의 기본 구조를 이해하는 데 큰 도움이 될 것입니다.

4.1. 시나리오 및 목표 설정

시나리오: 온라인 쇼핑몰에서 상품 재고 관리를 위해 매일 외부에서 받은 CSV 파일을 처리해야 합니다. 이 CSV 파일에는 상품 ID, 상품명, 현재 재고 수량이 포함되어 있습니다. 우리는 이 파일을 읽어서 재고 수량을 갱신하거나 새로운 상품 정보를 데이터베이스에 저장하는 배치 잡을 만들 것입니다.

목표:

  1. products.csv 파일 생성
  2. products.csv에서 상품 정보를 읽어오기 (ItemReader)
  3. 읽어온 상품 정보의 재고 수량을 특정 비율로 조정 (ItemProcessor)
  4. 조정된 상품 정보를 데이터베이스에 저장 (ItemWriter)
  5. 에러 처리 및 트랜잭션 관리의 기본 적용

4.2. 데이터베이스 스키마 및 모델 정의

1. 데이터베이스 스키마 (schema.sql)
H2 인메모리 데이터베이스를 사용할 것이므로, 애플리케이션 시작 시 자동으로 테이블을 생성하도록 schema.sql 파일을 만듭니다.

src/main/resources/schema.sql

DROP TABLE IF EXISTS product;

CREATE TABLE product (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL UNIQUE, -- 이름은 중복되지 않도록 UNIQUE 제약조건 추가
    stock_quantity INT NOT NULL,
    price INT NOT NULL
);

application.propertiesspring.batch.initialize-schema=always가 설정되어 있어 Spring Batch 메타데이터 테이블과 함께 schema.sql도 자동으로 실행됩니다.

2. 상품 모델 (Product.java)
CSV 파일에서 읽어오고 데이터베이스에 저장할 Product 객체를 정의합니다.

src/main/java/com/example/springbatchhelloworld/Product.java

package com.example.springbatchhelloworld;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@NoArgsConstructor
@ToString
public class Product {
    private Long id; // DB 저장 시 자동 생성되므로, CSV에서는 사용하지 않을 수 있음
    private String name;
    private int stockQuantity;
    private int price; // 가격 정보 추가 (가공 예시를 위해)

    // CSV 파일에서 읽어올 때 사용될 생성자 (id는 제외)
    public Product(String name, int stockQuantity, int price) {
        this.name = name;
        this.stockQuantity = stockQuantity;
        this.price = price;
    }
}

4.3. 입력 CSV 파일 생성 (products.csv)

src/main/resources/products.csv 파일을 생성하고 다음 내용을 추가합니다.

Laptop,100,1200000
Mouse,500,25000
Keyboard,200,80000
Monitor,70,300000
Webcam,150,50000
InvalidProduct,,100000 ; 이 라인은 의도적으로 오류를 유발하여 에러 처리 테스트 (stockQuantity 누락)
Speaker,120,70000

InvalidProduct 라인은 stock_quantity 필드가 비어 있어 파싱 오류를 발생시키도록 의도적으로 추가했습니다.

4.4. 배치 설정 클래스 업데이트 (BatchConfig.java)

기존 BatchConfig.java를 수정하여 CSV to DB 시나리오에 맞게 ItemReader, ItemProcessor, ItemWriter를 재정의하고, 새로운 JobStep을 만듭니다.

package com.example.springbatchhelloworld;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.LineMapper;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource; // DataSource 추가

@Configuration
public class BatchConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final DataSource dataSource; // DataSource 주입

    public BatchConfig(JobRepository jobRepository, PlatformTransactionManager transactionManager, DataSource dataSource) {
        this.jobRepository = jobRepository;
        this.transactionManager = transactionManager;
        this.dataSource = dataSource;
    }

    // 기존 personItemReader, personItemWriter, processPersonStep, importUserJob은 주석 처리 또는 삭제하고,
    // 아래 새로운 Product 관련 빈들을 추가합니다.

    // 1. ItemReader: products.csv 파일에서 Product 객체를 읽어옵니다.
    @Bean
    public FlatFileItemReader<Product> productItemReader(LineMapper<Product> productLineMapper) {
        return new FlatFileItemReaderBuilder<Product>()
                .name("productItemReader")
                .resource(new ClassPathResource("products.csv"))
                .linesToSkip(0) // 헤더가 없으므로 0줄 스킵 (있으면 1로 설정)
                .encoding("UTF-8")
                .lineMapper(productLineMapper) // 정의한 lineMapper 사용
                .strict(false) // 파싱 오류 발생 시 Job 실패 대신 해당 라인을 건너뛰고 다음 처리 (에러 처리 예시)
                .build();
    }

    // CSV 라인을 Product 객체로 매핑하는 LineMapper 정의
    @Bean
    public LineMapper<Product> productLineMapper() {
        DefaultLineMapper<Product> lineMapper = new DefaultLineMapper<>();

        // 쉼표로 구분된 라인을 토큰으로 분리
        DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
        lineTokenizer.setNames("name", "stockQuantity", "price"); // CSV 컬럼명
        lineTokenizer.setStrict(false); // 토큰 개수가 일치하지 않아도 예외 발생 대신 null 반환

        // 토큰을 Product 객체의 필드에 매핑
        BeanWrapperFieldSetMapper<Product> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
        fieldSetMapper.setTargetType(Product.class);

        lineMapper.setLineTokenizer(lineTokenizer);
        lineMapper.setFieldSetMapper(fieldSetMapper);
        return lineMapper;
    }


    // 2. ItemProcessor: 읽어온 Product 객체를 가공합니다.
    @Bean
    public ItemProcessor<Product, Product> productItemProcessor() {
        return product -> {
            // 재고 수량을 10% 증가시키고 가격을 5% 인상하는 비즈니스 로직 적용
            product.setStockQuantity((int) (product.getStockQuantity() * 1.1));
            product.setPrice((int) (product.getPrice() * 1.05));
            System.out.println("가공된 상품: " + product);
            return product;
        };
    }

    // 3. ItemWriter: 가공된 Product 객체를 데이터베이스에 저장합니다.
    @Bean
    public JdbcBatchItemWriter<Product> productItemWriter() {
        return new JdbcBatchItemWriterBuilder<Product>()
                .dataSource(dataSource) // 주입받은 DataSource 사용
                .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>()) // Product 객체 필드를 SQL 파라미터로 매핑
                // 데이터가 이미 존재하면 업데이트, 없으면 삽입 (UPSERT) 쿼리
                // H2 데이터베이스에서는 ON CONFLICT 대신 MERGE INTO를 사용할 수도 있습니다.
                .sql("MERGE INTO product KEY(name) VALUES (:name, :stockQuantity, :price)")
                .build();
    }


    // Step 정의: 상품 데이터 처리 Step
    @Bean
    public Step processAndSaveProductStep(
            FlatFileItemReader<Product> productItemReader, // ItemReader 빈 주입
            ItemProcessor<Product, Product> productItemProcessor, // ItemProcessor 빈 주입
            JdbcBatchItemWriter<Product> productItemWriter // ItemWriter 빈 주입
    ) {
        return new StepBuilder("processAndSaveProductStep", jobRepository)
                .<Product, Product>chunk(5, transactionManager) // 청크 단위 5개, 트랜잭션 매니저 주입
                .reader(productItemReader)
                .processor(productItemProcessor)
                .writer(productItemWriter)
                // 에러 처리 설정 예시: FlatFileParseException 발생 시 5번까지 스킵 허용
                .faultTolerant() // 오류 허용 모드 활성화
                .skipLimit(5) // 스킵할 수 있는 오류의 최대 횟수
                .skip(org.springframework.batch.item.file.FlatFileParseException.class) // 특정 예외 발생 시 스킵
                .noRollback(org.springframework.batch.item.file.FlatFileParseException.class) // 특정 예외 발생 시 롤백하지 않음
                .build();
    }

    // Job 정의: 상품 데이터 가져와 DB에 저장하는 전체 Job
    @Bean
    public Job importProductJob(Step processAndSaveProductStep) {
        return new JobBuilder("importProductJob", jobRepository)
                .incrementer(new RunIdIncrementer()) // JobParameters를 매번 다르게 생성
                .start(processAndSaveProductStep) // 첫 번째 Step으로 상품 처리 Step 설정
                .build();
    }
}

업데이트된 코드 설명:

  • DataSource 주입: JdbcBatchItemWriter가 데이터베이스에 접근할 수 있도록 DataSource를 주입받습니다.
  • productItemReader():
    • resource(new ClassPathResource("products.csv")): 읽어올 CSV 파일을 products.csv로 변경했습니다.
    • lineMapper(productLineMapper): CSV 라인을 Product 객체로 매핑하기 위해 별도의 @Bean으로 정의한 productLineMapper를 사용합니다.
    • strict(false): 에러 처리의 기본 설정입니다. 파일 파싱 중 오류(예: 예상되는 필드 개수 불일치, 타입 변환 실패)가 발생하더라도 Job을 즉시 실패시키지 않고, 해당 라인을 건너뛰고 다음 라인을 계속 처리하도록 합니다.
  • productLineMapper(): CSV 라인을 Product 객체로 매핑하는 상세 로직을 캡슐화했습니다. DelimitedLineTokenizerBeanWrapperFieldSetMapper를 사용하여 CSV 컬럼을 Product 객체의 필드에 매핑합니다.
    • lineTokenizer.setStrict(false): 필드 개수가 일치하지 않아도 예외를 던지는 대신 null 필드 값을 반환하도록 설정하여 유연성을 높입니다.
  • productItemProcessor():
    • 람다식을 사용하여 ItemProcessor를 구현했습니다. Product 객체를 받아서 재고 수량을 10% 증가시키고 가격을 5% 인상하는 간단한 비즈니스 로직을 적용한 후, 가공된 Product 객체를 반환합니다. 이 ItemProcessornull을 반환하면 해당 ProductItemWriter로 전달되지 않고 필터링됩니다.
  • productItemWriter():
    • JdbcBatchItemWriterBuilder를 사용하여 JDBC 기반의 일괄 쓰기 ItemWriter를 구성합니다.
    • dataSource(dataSource): 주입받은 DataSource를 사용합니다.
    • itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>()): Product 객체의 필드 이름을 사용하여 SQL 쿼리의 :name, :stockQuantity와 같은 명명된 파라미터에 값을 자동으로 매핑하도록 합니다.
    • sql(...): 데이터베이스에 실행할 SQL 쿼리를 정의합니다. 여기서는 H2 데이터베이스에서 UPSERT(Update-Else-Insert) 기능을 제공하는 MERGE INTO 쿼리를 사용했습니다. name을 기준으로 데이터가 존재하면 업데이트, 없으면 삽입됩니다.
  • processAndSaveProductStep():
    • chunk(5, transactionManager): 청크 단위를 5로 설정했습니다. 즉, 5개의 Product 객체를 읽고 처리한 후 한 번에 데이터베이스에 기록하며, 이 과정 전체가 하나의 트랜잭션으로 묶입니다.
    • 에러 처리 (faultTolerant()):
      • faultTolerant(): 이 Step이 오류를 허용하는 방식으로 동작하도록 설정합니다.
      • skipLimit(5): 특정 예외가 최대 5번까지 발생해도 Job을 실패시키지 않고 건너뛰도록 합니다.
      • skip(FlatFileParseException.class): FlatFileParseException (파일 파싱 중 발생하는 오류)이 발생하면 해당 레코드를 건너뛰도록 지정합니다.
      • noRollback(FlatFileParseException.class): FlatFileParseException이 발생하더라도 현재 청크의 트랜잭션을 롤백하지 않도록 합니다. (주의: 일반적인 비즈니스 로직 오류에는 적용하지 않는 것이 좋으며, 데이터 무결성에 영향을 주지 않는 특정 유형의 오류에만 제한적으로 사용해야 합니다.)
  • importProductJob(): JobBuilder를 사용하여 importProductJob을 정의하고, processAndSaveProductStep을 첫 번째 Step으로 연결합니다.

4.5. Job 실행기 업데이트 (JobRunner.java)

JobRunner 클래스에서 importProductJob을 주입받아 실행하도록 수정합니다.

src/main/java/com/example/springbatchhelloworld/JobRunner.java

package com.example.springbatchhelloworld;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class JobRunner implements CommandLineRunner {

    private final JobLauncher jobLauncher;
    private final Job importProductJob; // importProductJob을 주입받습니다.

    public JobRunner(JobLauncher jobLauncher, Job importProductJob) {
        this.jobLauncher = jobLauncher;
        this.importProductJob = importProductJob;
    }

    @Override
    public void run(String... args) throws Exception {
        JobParameters jobParameters = new JobParametersBuilder()
                .addLong("time", System.currentTimeMillis())
                .toJobParameters();

        try {
            System.out.println("------------------------------------");
            System.out.println("Spring Batch Job 실행 시작: " + importProductJob.getName());
            jobLauncher.run(importProductJob, jobParameters); // importProductJob 실행
            System.out.println("Spring Batch Job 실행 완료!");
            System.out.println("------------------------------------");
        } catch (Exception e) {
            System.err.println("Spring Batch Job 실행 중 오류 발생: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

4.6. 애플리케이션 실행 및 결과 확인

애플리케이션을 실행하면 다음과 같은 로그를 콘솔에서 확인할 수 있습니다.

예상되는 콘솔 출력:

------------------------------------
Spring Batch Job 실행 시작: importProductJob
------------------------------------
... (Spring Batch 내부 로그) ...
읽어온 데이터: Product(id=null, name=Laptop, stockQuantity=100, price=1200000)
가공된 상품: Product(id=null, name=Laptop, stockQuantity=110, price=1260000)
읽어온 데이터: Product(id=null, name=Mouse, stockQuantity=500, price=25000)
가공된 상품: Product(id=null, name=Mouse, stockQuantity=550, price=26250)
읽어온 데이터: Product(id=null, name=Keyboard, stockQuantity=200, price=80000)
가공된 상품: Product(id=null, name=Keyboard, stockQuantity=220, price=84000)
... (청크 단위로 계속 출력) ...
읽어온 데이터: Product(id=null, name=Webcam, stockQuantity=150, price=50000)
가공된 상품: Product(id=null, name=Webcam, stockQuantity=165, price=52500)
읽어온 데이터: Product(id=null, name=InvalidProduct, stockQuantity=0, price=100000) <-- stockQuantity 파싱 오류로 0으로 기본값 설정됨
가공된 상품: Product(id=null, name=InvalidProduct, stockQuantity=0, price=105000) <-- 0 * 1.1 = 0으로 처리
... (FlatFileParseException에 대한 로그 또는 경고, 하지만 Job은 계속 진행됨) ...
읽어온 데이터: Product(id=null, name=Speaker, stockQuantity=120, price=70000)
가공된 상품: Product(id=null, name=Speaker, stockQuantity=132, price=73500)
... (Spring Batch 내부 로그) ...
Spring Batch Job 실행 완료!
------------------------------------

데이터베이스 확인:
애플리케이션을 실행한 후, http://localhost:8080/h2-console에 접속하여 (JDBC URL: jdbc:h2:mem:testdb, User Name: sa, Password: ) 다음 쿼리를 실행하여 저장된 데이터를 확인할 수 있습니다.

SELECT * FROM product;

결과를 보면 InvalidProduct를 포함한 모든 상품이 데이터베이스에 저장되었음을 알 수 있습니다. InvalidProductstock_quantity가 CSV에서는 비어 있었지만, FlatFileItemReaderstrict(false) 설정과 BeanWrapperFieldSetMapper의 기본 동작으로 인해 0으로 파싱되어 처리된 것을 볼 수 있습니다. 만약 더 엄격한 에러 처리를 원한다면 ItemProcessor에서 null 체크 또는 유효성 검사를 추가하여 null을 반환함으로써 해당 Item을 필터링할 수도 있습니다.

이 예제를 통해 여러분은 CSV 파일을 읽어 가공한 후 데이터베이스에 저장하는 실용적인 Spring Batch 잡을 성공적으로 구현해 보았습니다. 특히 ItemReader, ItemProcessor, JdbcBatchItemWriter의 사용법과 faultTolerant를 이용한 기본적인 에러 처리 방식을 익혔습니다. 다음 섹션에서는 실제 운영 환경에서 Spring Batch 잡을 효율적으로 관리하고 성능을 최적화하는 고급 팁들을 살펴보겠습니다.


5. Spring Batch 운영 및 성능 최적화 Best Practice

Spring Batch는 강력한 데이터 처리 도구지만, 실제 운영 환경에 적용할 때는 단순히 잡을 만드는 것을 넘어선 다양한 고려사항이 필요합니다. 이 섹션에서는 배치 잡의 안정적인 운영을 위한 모니터링, 재시작 및 실패 관리, 스케줄링 방법과 더불어, 대용량 데이터를 더욱 빠르게 처리하기 위한 스프링 배치 성능 최적화 전략들을 소개합니다. 이는 실무 적용을 위한 고급 팁과 best practice를 포함합니다.

5.1. 배치 잡 모니터링 및 관리

배치 잡은 한 번 실행되면 수십 분에서 수 시간 동안 독립적으로 동작할 수 있습니다. 따라서 잡의 현재 상태, 성공 여부, 실패 원인 등을 파악하는 것이 매우 중요합니다.

  • JobRepository 활용: Spring Batch는 모든 Job 실행 정보를 JobRepository를 통해 데이터베이스에 저장합니다. 이 정보들은 BATCH_JOB_INSTANCE, BATCH_JOB_EXECUTION, BATCH_STEP_EXECUTION 등과 같은 메타데이터 테이블에 기록됩니다.
  • JobExplorer / JobOperator:
    • JobExplorer: JobRepository에 저장된 메타데이터를 조회할 수 있는 읽기 전용 인터페이스입니다. 이를 통해 현재 실행 중인 Job의 목록, Job의 상태(STARTED, COMPLETED, FAILED 등), Step별 진행 상황 등을 프로그래밍 방식으로 확인할 수 있습니다.
    • JobOperator: JobExplorer보다 더 많은 기능을 제공하여, 실행 중인 Job을 중지(stop)하거나, 재시작(restart)하는 등 Job을 직접 제어할 수 있는 인터페이스입니다.
  • 커스텀 모니터링 대시보드: 실제 운영 환경에서는 JobExplorerJobOperator를 활용하여 자체적인 웹 기반 모니터링 대시보드를 구축하는 경우가 많습니다.

5.2. 재시작 및 실패 관리

Spring Batch의 가장 큰 장점 중 하나는 실패 시 재시작(Restartability) 기능입니다. 이를 효과적으로 활용하려면 몇 가지 원칙을 따라야 합니다.

  • JobParameters의 고유성: JobParametersJobInstance를 식별하는 기준이 됩니다. 재시작 시에는 이전 JobInstance와 동일한 JobParameters를 사용해야 합니다. JobRunner에서 new RunIdIncrementer()를 사용하면 매번 새로운 JobInstance가 생성되므로, 재시작 시에는 이 부분을 주의하여, 실패한 JobInstanceJobParameters를 그대로 사용해야 합니다.
  • Step의 상태: Spring Batch는 각 Step의 실행 상태(StepExecution)를 기록합니다. Step이 COMPLETED 상태가 아니라면, Job 재시작 시 해당 Step부터 다시 시작합니다. 이미 완료된 Step은 재실행하지 않고 건너뜁니다.
  • Idempotency (멱등성): ItemProcessorItemWriter는 멱등적으로 설계하는 것이 좋습니다. 즉, 같은 데이터를 여러 번 처리하거나 기록해도 결과가 동일하거나 일관성이 유지되도록 해야 합니다. 예를 들어, ItemWriter에서 INSERT 대신 UPSERT(INSERT OR UPDATE) 쿼리를 사용하는 것이 좋은 예입니다.
  • 트랜잭션 관리: chunk 단위로 트랜잭션이 관리되므로, 특정 청크에서 실패하면 해당 청크만 롤백됩니다. 이미 성공적으로 커밋된 이전 청크의 데이터는 유지됩니다.

5.3. 배치 잡 스케줄링

배치 잡은 특정 시점에 주기적으로 실행되어야 하는 경우가 많습니다.

  • 내장 스케줄러 (Spring @Scheduled): 간단한 주기적 실행에는 Spring의 @Scheduled 어노테이션을 사용할 수 있습니다. JobLauncher를 주입받아 @Scheduled 메서드 내에서 Job을 실행하는 방식입니다.이 경우 application.propertiesspring.batch.job.enabled=false를 유지하고, main 클래스에 @EnableScheduling 어노테이션을 추가해야 합니다.
  • @Component public class ProductBatchScheduler { private final JobLauncher jobLauncher; private final Job importProductJob; public ProductBatchScheduler(JobLauncher jobLauncher, Job importProductJob) { this.jobLauncher = jobLauncher; this.importProductJob = importProductJob; } // 매일 새벽 2시에 실행 @Scheduled(cron = "0 0 2 * * *") public void runProductImportJob() throws Exception { JobParameters jobParameters = new JobParametersBuilder() .addLong("time", System.currentTimeMillis()) .toJobParameters(); jobLauncher.run(importProductJob, jobParameters); } }
  • 외부 스케줄러: 복잡한 스케줄링 요구사항(예: 잡 간의 의존성, 분산 환경)에는 Cron, Quartz, Jenkins, Apache Airflow, Spring Cloud Task/Data Flow와 같은 외부 솔루션을 사용하는 것이 일반적입니다. 이들은 배치 애플리케이션을 별도의 JAR로 배포하고, 외부 스케줄러가 특정 시간에 java -jar your-batch-app.jar job.name=yourJob과 같이 실행 명령을 내리는 방식입니다.

5.4. 성능 최적화를 위한 병렬 처리 전략

대용량 데이터 처리는 시간이 오래 걸릴 수 있으므로, 성능 최적화는 매우 중요합니다. Spring Batch는 다양한 병렬 처리 전략을 제공합니다. 스프링 배치 성능 최적화의 핵심입니다.

  1. 청크 크기 (Chunk Size) 최적화:
    • 가장 기본적인 성능 튜닝 요소입니다. 청크 크기가 너무 작으면 트랜잭션 커밋 오버헤드가 커지고, 너무 크면 메모리 부족(OutOfMemoryError) 위험이 커지며, 실패 시 롤백 범위가 넓어집니다.
    • 이상적인 청크 크기는 데이터 특성, 시스템 자원(메모리, DB IO), 트랜잭션 오버헤드 등을 고려하여 테스트를 통해 찾아야 합니다. 보통 수백에서 수천 단위가 적절합니다.
  2. 데이터베이스 성능 튜닝:
    • ItemReader의 쿼리 최적화 (인덱스 사용, 불필요한 조인 제거).
    • ItemWriter의 배치 삽입/업데이트 (JDBC addBatch()) 활용: JdbcBatchItemWriter는 기본적으로 배치 기능을 사용하므로 성능이 좋습니다.
    • DB 인덱스, 파티셔닝, 캐싱 등 DB 자체의 성능 최적화.
  3. 병렬 처리 (Parallel Processing):
    • 멀티스레딩 스텝 (Multi-threading Step):
      • 단일 JVM 내에서 여러 스레드를 사용하여 ItemReader, ItemProcessor, ItemWriter를 병렬로 실행합니다. 가장 쉽게 적용할 수 있는 병렬 처리 방식입니다.
      • StepBuildertaskExecutor()를 사용하여 TaskExecutor를 지정합니다.
        @Bean
        public Step multiThreadedStep(
        JobRepository jobRepository,
        PlatformTransactionManager transactionManager,
        ItemReader<Product> reader,
        ItemProcessor<Product, Product> processor,
        ItemWriter<Product> writer) {
        return new StepBuilder("multiThreadedStep", jobRepository)
                .<Product, Product>chunk(10, transactionManager)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .taskExecutor(taskExecutor()) // Custom TaskExecutor 주입
                .build();
        }
        
      @Bean
      public TaskExecutor taskExecutor() {}
    • * **주의사항:** `ItemReader`가 스레드-세이프(thread-safe)해야 합니다. `JdbcCursorItemReader`와 같은 일부 Reader는 단일 스레드 전용이므로 `JdbcPagingItemReader`와 같은 스레드-세이프 Reader를 사용해야 합니다. `ItemProcessor`와 `ItemWriter`도 상태가 없는(stateless) 방식으로 구현하여 스레드-세이프하게 만듭니다.
    • ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); // 기본 스레드 수 executor.setMaxPoolSize(10); // 최대 스레드 수 executor.setQueueCapacity(25); // 큐 사이즈 executor.setThreadNamePrefix("batch-thread-"); executor.initialize(); return executor;
    • 파티셔닝 (Partitioning Step):
      • 단일 Step을 여러 개의 독립적인 StepExecution으로 분할하여 병렬로 실행하는 방식입니다. 주로 ItemReader에서 읽을 데이터 범위를 나누어 각 파티션이 독립적으로 처리하도록 합니다.
      • 예를 들어, 100만 건의 데이터 중 처음 50만 건은 '파티션 1'이, 나머지 50만 건은 '파티션 2'가 처리하도록 나눌 수 있습니다. 각 파티션은 다른 스레드나 심지어 다른 서버에서 실행될 수 있습니다.
      • PartitionHandlerPartitioner를 구현해야 하므로 멀티스레딩 스텝보다 설정이 복잡합니다. 하지만 데이터 소스를 물리적으로 분리하여 처리할 수 있어 대용량 데이터 처리 및 분산 환경에 적합합니다.
    • 원격 청킹 (Remote Chunking):
      • ItemReaderItemProcessor는 마스터(Master) 프로세스에서 실행하고, ItemWriter는 하나 이상의 원격 슬레이브(Slave) 프로세스에서 실행하는 분산 처리 방식입니다.
      • 마스터는 읽은 아이템 청크를 메시지 큐(Kafka, RabbitMQ 등)를 통해 슬레이브에게 보내고, 슬레이브는 이 청크를 받아 처리 후 데이터베이스에 기록합니다.
      • 가장 복잡한 설정이 필요하지만, 가장 높은 확장성을 제공하여 매우 큰 데이터를 여러 서버에 분산하여 처리할 때 사용됩니다.

5.5. Best Practice 요약

  • 재시작 가능성(Restartability)은 필수: 모든 배치 잡은 실패 시 재시작할 수 있도록 설계해야 합니다.
  • 작은 청크 크기부터 시작: 처음부터 너무 큰 청크 크기를 설정하기보다 작은 청크부터 시작하여 점진적으로 최적화합니다.
  • 멱등성(Idempotency) 유지: ItemProcessorItemWriter는 여러 번 실행되어도 동일한 결과를 보장하도록 만듭니다.
  • 명확한 로깅 전략: 배치 잡의 진행 상황, 성공/실패, 에러 상세 내역을 명확하게 기록하는 로깅 전략을 수립합니다.
  • 충분한 성능 테스트: 실제와 유사한 데이터 볼륨으로 성능 테스트를 충분히 수행하여 최적의 설정을 찾습니다.
  • 적절한 트랜잭션 격리 수준 설정: 배치 작업은 장시간 실행될 수 있으므로, 다른 트랜잭션에 미치는 영향을 최소화합니다.

Spring Batch는 강력하고 유연한 프레임워크이지만, 그만큼 올바른 사용법과 운영 지식을 요구합니다. 이 섹션에서 다룬 모니터링, 재시작 전략, 스케줄링, 그리고 다양한 병렬 처리 기법들을 숙지하고 적용함으로써, 여러분의 배치 시스템은 더욱 견고하고 효율적으로 대용량 데이터를 처리할 수 있을 것입니다.


결론: Spring Boot Batch로 데이터 처리 자동화의 전문가가 되세요!

지금까지 우리는 Spring Boot Batch의 모든 것을 탐구해 보았습니다. "스프링 배치 란" 무엇인지 그 근본적인 필요성부터 시작하여, Job, Step, ItemReader, ItemProcessor, ItemWriter와 같은 핵심 개념들을 쉽고 명확하게 이해했습니다. 또한, 실제 "spring boot batch 예제"를 통해 CSV 파일을 읽어 콘솔에 출력하는 간단한 잡부터, "spring batch csv to db" 시나리오를 구현하며 "spring boot batch 대용량 처리"의 기반을 다졌습니다. 마지막으로는 "스프링 배치 성능 최적화"를 위한 모니터링, 재시작, 스케줄링, 그리고 멀티스레딩과 파티셔닝 같은 고급 병렬 처리 기법들까지 폭넓게 다루었습니다.

Spring Batch는 복잡하고 반복적인 대용량 데이터 처리 작업을 자동화하고, 안정적으로 관리하며, 미래의 확장성까지 고려할 수 있게 해주는 매우 효과적인 도구입니다. 이 가이드를 통해 여러분은 단순한 개발자를 넘어, 데이터 처리 자동화의 핵심 지식과 실질적인 구현 방법을 습득했을 것입니다.

데이터의 시대에, 효율적인 데이터 처리 능력은 그 어떤 기술보다도 강력한 경쟁력이 됩니다. 오늘 배운 지식들을 바탕으로 여러분의 실제 프로젝트에 Spring Boot Batch를 적용해 보세요. 처음에는 어렵게 느껴질 수 있지만, 한 단계씩 직접 구현해보고 문제를 해결해나가다 보면 어느새 능숙하게 대용량 데이터를 다루는 자신을 발견하게 될 것입니다.

이 가이드가 여러분의 데이터 처리 여정에 든든한 나침반이 되기를 바랍니다. 더 깊이 있는 학습과 실전 경험을 통해 Spring Batch의 무한한 가능성을 탐험해 나가시길 응원합니다! Happy Batching!

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함