티스토리 뷰
반응형
정산 시스템을 개발할 때는 1) 대용량 데이터를 메모리 누수(OOM) 없이 안전하게 읽어오는 것, 2) 정확한 금액 연산(BigDecimal), 그리고 3) 대량 데이터 일괄 처리(Bulk Update)가 매우 중요합니다.
이번 글에서는 Spring Batch의 Chunk 기반 프로세싱과 MyBatis CursorItemReader를 결합하여 대용량 정산 배치를 구현하는 방법을 코드를 통해 알아보겠습니다.
1. 개발 환경 및 사전 준비
- Java 17+
- Spring Boot 3.x
- Spring Batch
- MyBatis
- PostgreSQL / MySQL
테스트용 테이블 스키마 (DDL)
SQL
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
order_no VARCHAR(50) NOT NULL,
amount NUMERIC(15, 2) NOT NULL, -- 정밀한 금액 계산을 위한 NUMERIC 타입
fee NUMERIC(15, 2), -- 정산 수수료
status VARCHAR(20) DEFAULT 'READY'
);
2. 전체 소스 코드 구현
① 정산 대상 VO (OrderVo.java)
금액 다루기에 필수적인 BigDecimal 타입을 사용합니다.
Java
package com.example.batch.domain;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class OrderVo {
private Long id;
private String orderNo;
private BigDecimal amount; // 주문 금액
private BigDecimal fee; // 계산된 수수료
}
② MyBatis Mapper 인터페이스 및 XML
데이터를 커서 방식으로 읽어올 쿼리와 1,000건씩 일괄 수정(Batch Update)할 쿼리를 작성합니다.
OrderMapper.java
Java
package com.example.batch.mapper;
import com.example.batch.domain.OrderVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface OrderMapper {
// 배치 리더용 (전체 대상 조회)
List<OrderVo> selectOrdersForSettlement();
// 청크 단위 일괄 업데이트용
void updateSettlementResult(@Param("list") List<? extends OrderVo> list);
}
OrderMapper.xml
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.batch.mapper.OrderMapper">
<!-- 커서 기반 조회를 위해 전체 목록을 조회하는 쿼리 (실제로는 페이징/조건절 튜닝 필요) -->
<select id="selectOrdersForSettlement" resultType="com.example.batch.domain.OrderVo">
SELECT id, order_no, amount, fee
FROM orders
WHERE status = 'READY'
</select>
<!-- 1000건씩 한 번에 업데이트하는 배치 쿼리 -->
<update id="updateSettlementResult">
<foreach collection="list" item="item" separator=";">
UPDATE orders
SET fee = #{item.fee},
status = 'COMPLETED'
WHERE id = #{item.id}
</foreach>
</update>
</mapper>
③ ItemProcessor (정산 수수료 계산 로직)
건별로 넘어오는 주문 데이터를 받아 BigDecimal을 이용해 안전하게 수수료를 계산합니다.
Java
package com.example.batch.processor;
import com.example.batch.domain.OrderVo;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
@Component
public class SettlementProcessor implements ItemProcessor<OrderVo, OrderVo> {
@Override
public OrderVo process(OrderVo order) throws Exception {
// 수수료 5% 계산 (원 미만 절사)
BigDecimal fee = order.getAmount()
.multiply(new BigDecimal("0.05"))
.setScale(0, RoundingMode.FLOOR);
order.setFee(fee);
return order; // 가공된 데이터는 ItemWriter로 전달됨
}
}
④ ItemWriter (DB 일괄 반영)
청크 단위(1,000건)로 모인 데이터를 받아 MyBatis를 통해 일괄 업데이트를 수행합니다.
Java
package com.example.batch.writer;
import com.example.batch.domain.OrderVo;
import com.example.batch.mapper.OrderMapper;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class SettlementWriter implements ItemWriter<OrderVo> {
@Autowired
private OrderMapper orderMapper;
@Override
public void write(Chunk<? extends OrderVo> chunk) throws Exception {
// 청크에 모인 데이터(예: 1000건)를 한 번에 전달하여 DB 부하 최소화
orderMapper.updateSettlementResult(chunk.getItems());
}
}
⑤ Spring Batch Job 설정 (SettlementBatchConfig.java)
메모리 부하를 막기 위해 MyBatis CursorItemReader를 설정하고, Chunk(1000) 단위로 작업을 조립합니다.
Java
package com.example.batch.config;
import com.example.batch.domain.OrderVo;
import com.example.batch.processor.SettlementProcessor;
import com.example.batch.writer.SettlementWriter;
import com.example.batch.mapper.OrderMapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.batch.MyBatisCursorItemReader;
import org.mybatis.spring.batch.builder.MyBatisCursorItemReaderBuilder;
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.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class SettlementBatchConfig {
/**
* 1. Job 정의: 배치 작업의 최상단 단위로 Step들을 실행합니다.
*/
@Bean
public Job settlementJob(JobRepository jobRepository, Step settlementStep) {
return new JobBuilder("settlementJob", jobRepository)
.start(settlementStep)
.build();
}
/**
* 2. Step 정의: 실질적인 읽기 -> 가공 -> 쓰기 작업이 이루어지는 단위입니다.
* 1000개 단위(Chunk)로 트랜잭션을 묶어서 처리합니다.
*/
@Bean
public Step settlementStep(JobRepository jobRepository,
PlatformTransactionManager transactionManager,
MyBatisCursorItemReader<OrderVo> settlementReader,
SettlementProcessor settlementProcessor,
SettlementWriter settlementWriter) {
return new StepBuilder("settlementStep", jobRepository)
.<OrderVo, OrderVo>chunk(1000, transactionManager) // 👉 1000건 단위 Chunk 처리
.reader(settlementReader) // DB 커서를 이용한 메모리 절약형 읽기
.processor(settlementProcessor) // 비즈니스 로직 (수수료 계산)
.writer(settlementWriter) // 1000건 모아서 일괄 쓰기
.build();
}
/**
* 3. ItemReader 정의: MyBatis Cursor를 이용해 대용량 데이터를 끊어서 읽어옵니다.
*/
@Bean
public MyBatisCursorItemReader<OrderVo> settlementReader(SqlSessionFactory sqlSessionFactory) {
return new MyBatisCursorItemReaderBuilder<OrderVo>()
.sqlSessionFactory(sqlSessionFactory)
.queryId("com.example.batch.mapper.OrderMapper.selectOrdersForSettlement")
.build();
}
/**
* 4. ItemProcessor 정의 (선택적 빈 등록)
* 컴포넌트 스캔(@Component)을 사용하지 않고 명시적으로 Bean을 등록할 수도 있습니다.
*/
@Bean
public SettlementProcessor settlementProcessor() {
return new SettlementProcessor();
}
/**
* 5. ItemWriter 정의 (선택적 빈 등록)
*/
@Bean
public SettlementWriter settlementWriter(OrderMapper orderMapper) {
return new SettlementWriter(orderMapper);
}
}
3. 핵심 정리 포인트
- OOM(메모리 부족) 방지: 수백만 건의 데이터를 List로 한 번에 조회하지 않고, MyBatisCursorItemReader를 활용해 DB 커서를 유지한 채 한 건씩 스트리밍 방식으로 읽어옵니다.
- 정확한 금액 연산: 부동소수점 오차가 발생하는 double 대신 BigDecimal과 setScale(0, RoundingMode.FLOOR)를 사용해 금융 정산의 정확성을 보장했습니다.
- 성능 최적화 (Chunk): 1건씩 처리하고 커밋하는 것이 아니라, 1,000건을 하나의 트랜잭션 청크(Chunk)로 묶어 처리함으로써 네트워크 통신 및 DB I/O 비용을 대폭 줄였습니다.
반응형
'배움 > JAVA' 카테고리의 다른 글
| REST API를 개발할 때 반드시 알아야 할 주요 애노테이션(Annotation) (0) | 2026.08.14 |
|---|---|
| [JavaScript] 순수 내장 기능(fetch + async/await)으로 GET, POST 비동기 통신 정리 (0) | 2026.07.03 |
| Spring Boot REST API 샘플 (0) | 2026.06.24 |
| 소켓통신 테스트 (0) | 2026.05.21 |
| Eclipse JAVA, Gradle, JPA 설정 (0) | 2025.06.27 |
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- WSL2
- mysql
- 명령어
- call_user_func
- eloquent
- ubuntu
- curl_multi_init
- laravel 테스트
- laravel 12
- 설치
- 단축키
- wsl
- ob_get_contents
- uniqid
- laravel 11
- facades
- swagger
- reflectionclass
- privatechannel
- eclipse
- 비동기
- flask
- fromArray
- array_combine
- PYTHON
- 설정
- Laravel
- jp:a
- OpenAI GPT
- php
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
글 보관함
