티스토리 뷰
Spring Batch로 배치 적재 기능을 개발하면서 Writer를 JPA로 구현할지 JDBC로 할지를 고민하게 되었습니다. 프로젝트에서 데이터 조회는 이미 JPA를 기반으로 하고 있었고 Spring Batch에서도 JpaItemWriter를 기본적으로 제공하므로 처음에는 자연스럽게 이를 선택했습니다. 실제로 코드도 간결했고 데이터량도 적다 보니 성능도 충분하다고 판단했습니다.
하지만 Writer를 JDBC 기반으로 변경하면 어떤 차이가 있을지 궁금해 JdbcBatchItemWriter로 동일한 작업을 수행해 보았고 결과적으로 예상 이상의 성능 향상 효과를 확인했습니다.
두 Writer의 성능을 비교하기 위해 청크 단위의 실행 시간을 측정하는 WriterTimingListener를 추가로 구현했습니다.
@Slf4j
public class WriterTimingListener implements ItemWriteListener<Weather> {
private Instant startTime;
@Override
public void beforeWrite(Chunk<? extends Weather> items) {
startTime = Instant.now();
log.info(">>>> Writer - 청크 쓰기 시작. 아이템 수: {}", items.size());
}
@Override
public void afterWrite(Chunk<? extends Weather> items) {
if (startTime == null) {
log.warn("Writer - 시작 시간이 기록되지 않았습니다. afterWrite가 먼저 호출되었습니다.");
return;
}
var endTime = Instant.now();
var duration = Duration.between(startTime, endTime);
var itemCount = items.size();
log.info("<<<< Writer - 청크 쓰기 완료. 소요 시간: {} ms, 아이템 수: {}",
duration.toMillis(), itemCount);
if (itemCount > 0) {
double avgTime = (double) duration.toMillis() / itemCount;
log.info("---- Writer - 아이템당 평균 처리 시간: {} ms", String.format("%.3f", avgTime));
}
}
@Override
public void onWriteError(Exception exception, Chunk<? extends Weather> items) {
log.error("!!!! Writer - 청크 쓰기 오류 발생. 아이템 수: {}, 오류: {}",
items.size(), exception.getMessage());
}
}
먼저 API 호출 후 가공 후 insert를 합니다. 데이터는 290개 중 97개의 데이터만 적재합니다.
3번씩 반복 테스트 통해 평균을 구해봤습니다.
JpaItemWriter 코드입니다. 비교적 깔끔하다고 생각됩니다.
@Bean
public Step weatherStep(WeatherItemReader reader, WeatherItemProcessor processor, JpaItemWriter<Weather> weatherJpaWriter, WriterTimingListener writerTimingListener) {
return new StepBuilder(JOB_NAME + "weatherStep", jobRepository)
.<WeatherItem, Weather>chunk(300, transactionManager)
.reader(reader)
.processor(processor)
.writer(weatherJpaWriter)
.listener(writerTimingListener)
.faultTolerant()
.retryLimit(3)
.retry(CoreException.class)
.build();
}
@Bean
public JpaItemWriter<Weather> weatherJpaWriter(EntityManagerFactory emf) {
JpaItemWriter<Weather> w = new JpaItemWriter<>();
w.setEntityManagerFactory(emf);
w.setUsePersist(false);
return w;
}
평균 소요 시간은 121.67 ms입니다. 아이템당 평균 처리 시간은 약 1.254 ms
소요 시간: 122 ms, 아이템 수: 97
아이템당 평균 처리 시간: 1.258 ms
소요 시간: 124 ms, 아이템 수: 97
아이템당 평균 처리 시간: 1.278 ms
소요 시간: 119 ms, 아이템 수: 97
아이템당 평균 처리 시간: 1.227 ms
JdbcBatchItemWriter 코드입니다. SQL이 작성되어 있습니다.
@Bean
public Step weatherStep(WeatherItemReader reader, WeatherItemProcessor processor, JdbcBatchItemWriter<Weather> weatherJdbcWriter, WriterTimingListener writerTimingListener) {
return new StepBuilder(JOB_NAME + "weatherStep", jobRepository)
.<WeatherItem, Weather>chunk(300, transactionManager)
.reader(reader)
.processor(processor)
.writer(weatherJdbcWriter)
.listener(writerTimingListener)
.faultTolerant()
.retryLimit(3)
.retry(CoreException.class)
.build();
}
@Bean
public JdbcBatchItemWriter<Weather> weatherJdbcWriter() {
String sql = "INSERT INTO OUTDOOR_WEATHER (COLLECT_DT, NX, NY, OUTDOOR_TYPE, VALUE, CREATE_DT) " +
"VALUES (:collectDt, :nx, :ny, :outdoorType, :value, NOW())";
return new JdbcBatchItemWriterBuilder<Weather>()
.dataSource(dataSource)
.sql(sql)
.itemSqlParameterSourceProvider(item -> new MapSqlParameterSource()
.addValue("collectDt", item.getCollectDt())
.addValue("nx", item.getNx())
.addValue("ny", item.getNy())
.addValue("outdoorType", item.getOutdoorType().name())
.addValue("value", item.getValue()))
.build();
}
평균 소요 시간은 11.33 ms입니다. 아이템당 평균 처리 시간은 약 0.0117ms
소요 시간: 13 ms, 아이템 수: 97
아이템당 평균 처리 시간: 0.134 ms
소요 시간: 12 ms, 아이템 수: 97
아이템당 평균 처리 시간: 0.124 ms
소요 시간: 9 ms, 아이템 수: 97
아이템당 평균 처리 시간: 0.093 ms
JdbcBatchItemWriter 사용 시, JpaItemWriter 대비 전체 처리 시간이 약 90% 감소했고 성능이 약 10배 이상 향상되었습니다. 성능 차이가 발생한 이유는 JPA는 기본적으로 엔티티 상태관리, 영속성 컨텍스트 등 부가적인 작업이 동반하기 때문입니다. 반면 JDBC는 ORM 관련 비용이 전혀 발생하지 않아 순수하게 데이터를 적재하는 작업에만 집중하기 때문입니다. Batch Writer에서는 JDBC 방식이 매우 효율적일 수 있다는 걸 이번 테스트를 통해 확인했습니다. JPA와 JDBC를 적절히 선택하는 전략이 필요하다는 걸 다시 한번 느끼게 되었습니다.
'Backend' 카테고리의 다른 글
클릭 대신 테라폼으로 인프라 관리하기 (0) | 2025.10.17 |
---|---|
다이어그램으로 보는 AWS VPC 통신 정리 (0) | 2025.10.08 |
왜 나는 RestClient를 선택했는가 (0) | 2025.09.23 |
쿠버네티스 핵심 개념 정리와 마이크로서비스 이해 (0) | 2025.09.19 |
Spring Boot에서 Redis 캐시 적용부터 Cluster 운영까지 (1) | 2025.09.12 |
- Total
- Today
- Yesterday
- bool
- counter
- Python
- zip
- operators
- If
- Upper
- Lower
- Built-in Functions
- isalpha
- index
- function
- isdigit
- Method
- Lambda
- permutations
- combinations
- for
- find
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |