🎯 예제 코드 만들어보기
저번 포스팅에선 학습한 내용을 기반으로 배치 프로세스를 만들어보겠습니다. (Spring Batch 5)
https://comumu.tistory.com/113
[Spring Batch] Chunk 와 ItemReader & ItemProcessor & ItemWriter
🎯 Chunk 란 무엇일까 ?Spring Batch 에서 Chunk 지향처리 (Chunk-Oriented Oricessing) 는 대용량의 데이터를 효율적으로 처리하기 위한 방법입니다. 데이터 처리 작업을 읽기, 처리, 쓰기 세 단계로 나눈 뒤
comumu.tistory.com
먼저 그전에 배운 내용을 간단히 정리하면 Spring Batch 는 Chunk 지향 처리를 합니다. 즉, 한 번에 하나씩 데이터를 읽어 Chunk 라는 데이터 덩어리를 만든 뒤, Chunk 단위로 트랜잭션을 다룹니다. Spring Batch 5 에서 이런 Chunk 처리를 어떻게 하는지 예제 코드를 통해 알아보겠습니다.
✅ build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'batch'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-batch'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.batch:spring-batch-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
✅ application.yml
spring:
datasource:
initialization-mode: always
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/batch?serverTimezone=Asia/Seoul
username: [username]
password: [password]
sql:
init:
mode: always
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
show_sql: true
format_sql: true
batch:
job:
enabled: true
name: customerJob
jdbc:
initialize-schema: always
logging:
level:
org.hibernate.SQL: debug
org.hibernate.type.descriptor.sql.BasicBinder: trace
DB 는 MySql 을 사용했고 실행할 Job 을 customerJob 으로 설정했습니다. 저는 DB 에 밑에서 구현할 Teacher 와 Student 를 미리 저장해 뒀습니다. 데이터 저장 쿼리가 필요하면 밑에 접은글을 참고하시면 될거같습니다.
INSERT INTO Teacher (name) VALUES ('Teacher 1');
INSERT INTO Teacher (name) VALUES ('Teacher 2');
INSERT INTO Teacher (name) VALUES ('Teacher 3');
INSERT INTO Teacher (name) VALUES ('Teacher 4');
INSERT INTO Teacher (name) VALUES ('Teacher 5');
INSERT INTO Teacher (name) VALUES ('Teacher 6');
INSERT INTO Teacher (name) VALUES ('Teacher 7');
INSERT INTO Teacher (name) VALUES ('Teacher 8');
INSERT INTO Teacher (name) VALUES ('Teacher 9');
INSERT INTO Teacher (name) VALUES ('Teacher 10');
INSERT INTO Teacher (name) VALUES ('Teacher 11');
INSERT INTO Teacher (name) VALUES ('Teacher 12');
INSERT INTO Teacher (name) VALUES ('Teacher 13');
INSERT INTO Teacher (name) VALUES ('Teacher 14');
INSERT INTO Teacher (name) VALUES ('Teacher 15');
INSERT INTO Teacher (name) VALUES ('Teacher 16');
INSERT INTO Teacher (name) VALUES ('Teacher 17');
INSERT INTO Teacher (name) VALUES ('Teacher 18');
INSERT INTO Teacher (name) VALUES ('Teacher 19');
INSERT INTO Teacher (name) VALUES ('Teacher 20');
INSERT INTO Student (name, teacher_id) VALUES ('Student 1', 1);
INSERT INTO Student (name, teacher_id) VALUES ('Student 2', 1);
INSERT INTO Student (name, teacher_id) VALUES ('Student 3', 2);
INSERT INTO Student (name, teacher_id) VALUES ('Student 4', 2);
INSERT INTO Student (name, teacher_id) VALUES ('Student 5', 3);
INSERT INTO Student (name, teacher_id) VALUES ('Student 6', 3);
INSERT INTO Student (name, teacher_id) VALUES ('Student 7', 4);
INSERT INTO Student (name, teacher_id) VALUES ('Student 8', 4);
INSERT INTO Student (name, teacher_id) VALUES ('Student 9', 5);
INSERT INTO Student (name, teacher_id) VALUES ('Student 10', 5);
INSERT INTO Student (name, teacher_id) VALUES ('Student 11', 6);
INSERT INTO Student (name, teacher_id) VALUES ('Student 12', 6);
INSERT INTO Student (name, teacher_id) VALUES ('Student 13', 7);
INSERT INTO Student (name, teacher_id) VALUES ('Student 14', 7);
INSERT INTO Student (name, teacher_id) VALUES ('Student 15', 8);
INSERT INTO Student (name, teacher_id) VALUES ('Student 16', 8);
INSERT INTO Student (name, teacher_id) VALUES ('Student 17', 9);
INSERT INTO Student (name, teacher_id) VALUES ('Student 18', 9);
INSERT INTO Student (name, teacher_id) VALUES ('Student 19', 10);
INSERT INTO Student (name, teacher_id) VALUES ('Student 20', 10);
INSERT INTO Student (name, teacher_id) VALUES ('Student 21', 11);
INSERT INTO Student (name, teacher_id) VALUES ('Student 22', 11);
INSERT INTO Student (name, teacher_id) VALUES ('Student 23', 12);
INSERT INTO Student (name, teacher_id) VALUES ('Student 24', 12);
INSERT INTO Student (name, teacher_id) VALUES ('Student 25', 13);
INSERT INTO Student (name, teacher_id) VALUES ('Student 26', 13);
INSERT INTO Student (name, teacher_id) VALUES ('Student 27', 14);
INSERT INTO Student (name, teacher_id) VALUES ('Student 28', 14);
INSERT INTO Student (name, teacher_id) VALUES ('Student 29', 15);
INSERT INTO Student (name, teacher_id) VALUES ('Student 30', 15);
INSERT INTO Student (name, teacher_id) VALUES ('Student 31', 16);
INSERT INTO Student (name, teacher_id) VALUES ('Student 32', 16);
INSERT INTO Student (name, teacher_id) VALUES ('Student 33', 17);
INSERT INTO Student (name, teacher_id) VALUES ('Student 34', 17);
INSERT INTO Student (name, teacher_id) VALUES ('Student 35', 18);
INSERT INTO Student (name, teacher_id) VALUES ('Student 36', 18);
INSERT INTO Student (name, teacher_id) VALUES ('Student 37', 19);
INSERT INTO Student (name, teacher_id) VALUES ('Student 38', 19);
INSERT INTO Student (name, teacher_id) VALUES ('Student 39', 20);
INSERT INTO Student (name, teacher_id) VALUES ('Student 40', 20);
✅ Teacher 클래스
package batch.example.chunk;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Entity
@Setter
@Getter
public class Teacher {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "teacher", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Student> students;
}
✅ Student 클래스
package batch.example.chunk;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Setter
@Getter
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "teacher_id")
private Teacher teacher;
}
Teacher 와 Student 를 1:N 연관관계로 매핑한 이유는 트랜잭션 범위를 확인 하기 위해 이처럼 설계했습니다. 이는 추후 다시 설명하겠습니다.
✅ BatchConfig 클래스
package batch.example.chunk;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.database.JdbcCursorItemReader;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.Locale;
@Component
public class BatchConfig {
/**
* DB 와 관계된 커넥션 정보를 담고 있으며 빈으로 등록하여 인자로 넘겨준다.
*/
private final DataSource dataSource;
public BatchConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* Cursor 기반 Reader
*/
@Bean
@StepScope
public JdbcCursorItemReader<Teacher> reader(){
JdbcCursorItemReader<Teacher> reader = new JdbcCursorItemReader<>();
reader.setDataSource(dataSource);
reader.setSql("SELECT id, name FROM teacher");
reader.setRowMapper(new BeanPropertyRowMapper<>(Teacher.class)); //DB 에서 읽어온 각 행의 데이터를 Teacher 객체로 변환하여 처리할 수 있도록 ㅘ는 설정
return reader;
}
/**
* 이름을 대문자로 바꾸는 비지니스 로직
*/
@Bean
public ItemProcessor<Teacher, String> processor() {
return teacher -> String.format("processor => %s", teacher.getName().toUpperCase(Locale.ROOT));
}
/**
* ItemWriter 인터페이스 람다 식으로 직접 구현
*/
@Bean
public ItemWriter<String> writer() {
return item -> System.out.println(String.join(", ", item));
}
}
BatchConfig 클래스에는 ItemReader, ItemProcessor, ItemWriter 를 정의했습니다. 이제 각각 메서드에 대한 코드를 살펴보겠습니다.
Reader
@Bean
@StepScope
public JdbcCursorItemReader<Teacher> reader(){
JdbcCursorItemReader<Teacher> reader = new JdbcCursorItemReader<>();
reader.setDataSource(dataSource);
reader.setSql("SELECT id, name FROM teacher");
reader.setRowMapper(new BeanPropertyRowMapper<>(Teacher.class)); //DB 에서 읽어온 각 행의 데이터를 Teacher 객체로 변환하여 처리할 수 있도록 ㅘ는 설정
return reader;
}
CursorItemReader 구현체인 JdbcCursorItemReader 로 구현해 Teacher 데이터를 읽어오는 구성입니다.
JdbcCursorItemReader<Teacher> reader = new JdbcCursorItemReader<>() : JdbcCursorItemReader 객체를 생성하고 이 리더는 JDBC 커서를 사용하여 데이터베이스에서 데이터를 순차적으로 읽어옵니다.
- reader.setDataSource(dataSource) : 데이터 베이스 연결 정보를 포함한 dataSource 를 넣어 JdbcCursorItemReader 가 DB 에 접근할 수 있게 합니다.
- reader.setSql() : SQL 쿼리를 설정합니다. Teacher 테이블에서 id 와 name 컬럼을 선택합니다.
- reader.setRowMapper(new BeanPropertyRowMapper<>(Teacher.class)) : 로우 매퍼(RowMapper) 를 설정합니다. 로우 매퍼는 데이터베이스 각 행을 Teacher 객체로 매핑하는 역할을 합니다.
Processor
/**
* 이름을 대문자로 바꾸는 비지니스 로직
*/
@Bean
public ItemProcessor<Teacher, String> processor() {
return teacher -> String.format("processor => %s", teacher.getName().toUpperCase(Locale.ROOT));
}
reader 에서 Teacher 를 읽은뒤 이름을 대문자로 바꿔 writer 로 넘기는 비지니스 로직입니다. 크게 어려운 부분이 없으니 넘어가겠습니다.
Writer
/**
* ItemWriter 인터페이스 람다 식으로 직접 구현
*/
@Bean
public ItemWriter<String> writer() {
return item -> System.out.println(String.join(", ", item));
}
writer 는 Spring Batch 에서 사용하는 출력 기능 입니다. writer 는 Chunk 단위로 트랜잭션이 실행되기때문에 위에서 구현한 쓰기 로직은 직접 지정한 Chunk 단위만큼 쌓이게 되면 실행됩니다. (Chunk 단위는 뒤에서 설정합니다.)
예제 코드에서는 단순히 출력하는 기능을 통해 Chunk 흐름 동작과정을 확인이 필요해 ItemWriter 인터페이스를 직접 커스텀해 구현했습니다.
✅ SimpleChunkJobConfiguration 클래스
package batch.example.chunk;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
@Slf4j
@RequiredArgsConstructor
@Configuration
public class SimpleChunkJobConfiguration {
@Autowired
private JobRepository jobRepository;
@Autowired
private PlatformTransactionManager platformTransactionManager;
@Autowired
private BatchConfig batchConfig;
@Bean
public Job customerJob() {
return new JobBuilder("customerJob", jobRepository)
.incrementer(new RunIdIncrementer())
.start(simpleChunkStep())
.build();
}
@Bean
public Step simpleChunkStep() {
return new StepBuilder("simpleChunkStep", jobRepository)
.<Teacher, String>chunk(10, platformTransactionManager)
.reader(batchConfig.reader())
.processor(batchConfig.processor())
.writer(batchConfig.writer())
.build();
}
}
SimpleChunkJobConfiguration 클래스에서는 Job 의 플로우를 정의했습니다.
- customerJob () : 시작 step 을 simpleChunkStep() 으로 정의했고, .incrementer(new RunIdIncrementer()) 은 배치 작업이 실행될 때마다 고유한 ID 를 생성하는 데 사용됩니다. 이렇게 하면 동일한 작업을 여러 번 실행할 때 각 실해잉 고유하게 식별될 수 있습니다.
- simpleChunkStep () : StepBuilder 를 생성자로 선언함과 동시에 반환했고, <Teacher, String> chunk(10, platformTransactionManager) 은 Reader 로 읽어오는 타입을 Teacher, Writer 로 쓰는 타입을 String 으로 정의했습니다. 또한 chunk 단위를 설정해 트랜잭션 단위가 10으로 실행됨을 명시해줬습니다.
자! 이제 배치 프로세스를 실행시켜볼까요?
2024-07-27T16:17:50.722+09:00 INFO 1861 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2024-07-27T16:17:50.725+09:00 INFO 1861 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 4307 ms
2024-07-27T16:17:51.053+09:00 WARN 1861 --- [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2024-07-27T16:17:51.823+09:00 INFO 1861 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2024-07-27T16:17:51.841+09:00 INFO 1861 --- [ main] batch.example.ExampleApplication : Started ExampleApplication in 6.103 seconds (process running for 6.559)
2024-07-27T16:17:51.847+09:00 INFO 1861 --- [ main] o.s.b.a.b.JobLauncherApplicationRunner : Running default command line with: []
2024-07-27T16:17:51.938+09:00 INFO 1861 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=customerJob]] launched with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}]
2024-07-27T16:17:51.967+09:00 INFO 1861 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [simpleChunkStep]
processor => TEACHER 1, processor => TEACHER 2, processor => TEACHER 3, processor => TEACHER 4, processor => TEACHER 5, processor => TEACHER 6, processor => TEACHER 7, processor => TEACHER 8, processor => TEACHER 9, processor => TEACHER 10
processor => TEACHER 11, processor => TEACHER 12, processor => TEACHER 13, processor => TEACHER 14, processor => TEACHER 15, processor => TEACHER 16, processor => TEACHER 17, processor => TEACHER 18, processor => TEACHER 19, processor => TEACHER 20
2024-07-27T16:17:52.013+09:00 INFO 1861 --- [ main] o.s.batch.core.step.AbstractStep : Step: [simpleChunkStep] executed in 46ms
2024-07-27T16:17:52.028+09:00 INFO 1861 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=customerJob]] completed with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 77ms
JobLanuncher 가 yml 파일에서 실행할 Job 을 선택해 실행하고, 우리가 지정한 Chunk 단위 (10) 만큼 콘솔창에 writer 되는 것을 확인할 수 있습니다.
Spring Batch 5 를 이용해 간단한 배치 프로세스를 만들어봤습니다.
'Spring > Spring Batch' 카테고리의 다른 글
[Spring Batch] Spring Batch 5 통합 테스트 설계 방법 (0) | 2024.08.15 |
---|---|
[Spring Batch] Spring Batch 5 예제 코드 (1) | 2024.08.14 |
[Spring Batch] Chunk 와 ItemReader & ItemProcessor & ItemWriter (0) | 2024.07.25 |
[Spring Batch] Job Scope 와 Job Parameter (0) | 2024.07.23 |
[Spring] (2) Spring Batch 5로 Job Flow 제어해보기 (7) | 2024.07.23 |