"결제 요청에서 수백 건의 데이터를 한꺼번에 처리해야 한다면?"
단건 처리 위주의 결제 로직에 익숙했던 저에게 이번 하위 결제 추가 작업은 새로운 도전이었습니다.
기존처럼 Batch에만 의존할 수 없는 실시간성 요구사항 속에서, 어떻게 하면 성능 저하 없이 대량의 데이터를 안전하게 적재할 수 있을지 고민했습니다
대용량 데이터 다루는 방법을 실험해보면서 사용한 Bulk Insert에 대해 정리해봅니다.
여러 개의 데이터 행(Row)을 단 하나의 INSERT 문장으로 합쳐서 DB에 한꺼번에 밀어 넣는 방식입니다.
① 네트워크 오버헤드 감소
DB와 서버 사이에도 통신 시간이 걸립니다. 1,000번 왕복할 시간을 1번으로 줄이니 응답 속도가 비약적으로 빨라집니다.
② 트랜잭션 및 로그 비용 절감
DB는 데이터를 넣을 때마다 기록을 남기고 Transaction을 관리하는데 Bulk Insert는 이 과정을 한 번에 처리하므로 DB 부하가 훨씬 적습니다.
③ 인덱스 재구성 최적화
데이터가 들어올 때마다 인덱스를 매번 다시 계산하는 게 아니라, 다 들어온 뒤에 한꺼번에 정리하므로 성능상 이점이 큽니다.
① 개별 Insert (Loop 방식)
INSERT INTO T_TRANS (id, amount) VALUES ('ID_001', 1000);
INSERT INTO T_TRANS (id, amount) VALUES ('ID_002', 2000);
...
② Bulk Insert
INSERT INTO T_TRANS (id, amount)
VALUES
('ID_001', 1000),
('ID_002', 2000),
... ;
개별 insert의 경우 java 내에서 for문을 돌려서 데이터 추가를 했습니다만
Bulk의 경우 한 번의 실행으로 처리할 수 있습니다.
대용량 데이터 처리가 빈번한 금융권 실무에서는
엑셀로 가공된 데이터를 수동으로 밀어 넣으며 오랜 시간을 기다리는 것이 일상이기도 합니다.
내가 처리를 했던 SQL문은 일반 insert문이 아니라 select insert문이다.
그렇게되면 일반 List로 처리하는 방식이 아니라 UNION ALL을 사용해야 했다.
① 일반
<insert id="insertTransList" parameterType="java.util.List">
INSERT INTO T_TRANS (
id, amount, ...
) VALUES
<foreach collection="list" item="item" separator=",">
(
#{item.id}
, #{item.amount}
...
)
</foreach>
</insert>
separator에 ','로 구분하여 VALUES를 여러 개 두면 되는 방식으로 기본적으로 생각했던 Bulk insert
② UNION ALL 사용
INSERT T_TRANS
(id, amount ...)
SELECT ...
FROM T_ITEM I
LEFT JOIN T_TRANS M ON I.id = M.id
WHERE id = 'ID_001'
UNION ALL
SELECT ...
FROM T_ITEM I
LEFT JOIN T_TRANS M ON I.id = M.id
WHERE id = 'ID_002'
....
해당 형식으로 mapper SQL문을 작성해야함으로 아래와 같이 작성하였다.
<insert id="insertTransList" parameterType="java.util.List">
INSERT T_TRANS (
(id, amount ...)
<foreach collection="list" item="item" separator=" UNION ALL ">
SELECT
#{item.id}
, #{item.amount}
...
FROM T_ITEM I
LEFT JOIN T_TRANS M ON I.id = M.id
WHERE id = #{item.id}
</foreach>
</insert>
< 전부 예시라서 SQL문 통을 보기보다는 전체적인 문장과 seperator에 집중 >
seperator에 UNION ALL을 추가하여 SELECT문을 이어주는 역할을 하고
foreach 안에 SELECT문을 넣어서 데이터 하나씩 가져온다.
< 차이점 >
일반적인 Bulk Insert는 VALUES 절을 나열하지만
이번 케이스는 기존 데이터를 참조(JOIN)하여 가공된 값을 추가해야 했기에 INSERT INTO ... SELECT ... UNION ALL 구조를 선택했습니다.
이를 통해 수백 번의 개별 쿼리를 단 하나의 트랜잭션으로 묶어 처리 효율을 극대화했습니다.
이번 쿼리에는 기존 로직을 계승한 LEFT JOIN이 포함되어 있었습니다.
GROUP BY 등을 활용해 쿼리 구조를 완전히 뜯어고칠 수도 있었지만 실무에서는 기존 비즈니스 검증 로직의 안정성을 유지하는 것도 중요합니다.
따라서 쿼리 구조를 변경하는 대신 조인 성능을 최적화하기 위해 연관 컬럼을 인덱스화하는 전략을 택했습니다.
결과적으로 코드의 가독성을 지키면서도 대용량 처리에 적합한 성능을 확보할 수 있었습니다.
DEVELOP 환경에 POSTMAN을 사용하려 대량 데이터 발사