1. 서론
Spring WebFlux + R2DBC + MariaDB 조합으로 서비스를 개발하다 보면, 대량 INSERT가 필요한 구간에서 성능 이슈를 마주하게 된다. 특히 “키 생성”, “로그 적재”, “배치성 데이터 적재”처럼 한 번에 수천~수만 건을 넣어야 하는 상황에서
- Reactive니까 알아서 빠르겠지 라는 기대와 달리,
- 실제로는 몇 분 단위로 시간이 걸리는 경우를 겪게 된다.
이 글은
- Spring WebFlux + R2DBC + MariaDB 환경에서
- 대량 INSERT 성능이 잘 나오지 않았던 원인을 분석하고,
- 다중 VALUES 기반 Bulk Insert 유틸리티로 성능을 개선한 사례와,
- 그 과정에서 고려해야 할 보안·유지보수 관점의 트레이드오프
까지 정리한 기록이다.
2. 문제 상황
개발하고 있는 차량 키 관리 솔루션에서 대칭 키를 대량으로 유도/생성/암호화하여 DB에 저장하는 기능이 있었고,
이에 대한 요구사항은 명확했다.
“10,000개 키 생성·저장을
**현실적인 시간 내(1분 이내)**에 끝낼 수 있게 만들 것.”
R2DBC에서 제공하는 API(repository.saveAll(Flux<...>))를 사용하였으나 10분이상 소요되는 문제가 발생하였고,
심지어 10,000건이 넘어가는 경우 DB Connection이 초과되는 에러가 발생했다.
3. R2DBC + MariaDB 조합에서의 제약
3-1. 숫자부터 확인: 어디가 느린가?
처음에는 “Reactive + 비동기니까 어느 정도는 빨라야 하지 않을까?”라는 막연한 기대가 있었다.
그래서 먼저, 다음 정도만 빠르게 계측했다.
- 키 생성 자체에 걸리는 시간
- DB INSERT 구간에 걸리는 시간
- 전체 배치 처리 시간
결과적으로:
- 키 생성 로직은 상대적으로 빠른 편이었고,
- INSERT 쿼리가 나가는 구간에서 지연이 집중되어 있었다.
즉, 이 문제는 알고리즘이나 비즈니스 로직이 아니라, DB 쓰기 패턴과 관련이 있었다.
3-2. “내 코드 탓인가?” – saveAll, 설정, 인덱스까지 의심
다음으로 의심한 건 순수하게 “내 코드/설정 문제”였다.
- Spring Data R2DBC의 saveAll/리액티브 리포지토리 패턴을 제대로 쓰고 있는지,
- 커넥션 풀 크기, 타임아웃 등 R2DBC 관련 설정이 적절한지,
- 인덱스 설계/실행 계획에서 쓸데없는 풀스캔이 있는지
간단히 했던 일들:
- EXPLAIN으로 INSERT 대상 테이블 인덱스 확인
- 불필요한 인덱스/트리거 여부 점검
- R2DBC 설정 조정 후 성능 재측정
- saveAll / DatabaseClient 여러 조합으로 실험
하지만 이런 조정을 통해서는 근본적인 성능 향상은 나오지 않았다.
체감상 조금 나아지는 정도에 그쳤고, “10분 → 1분” 수준의 개선과는 거리가 멀었다.
3-3. “그럼 R2DBC 쪽 한계일 수도?” – 레퍼런스 탐색
여기부터는 가설을 바꿨다.
“혹시 내가 API를 잘못 쓰고 있는 게 아니라,
아예 이 조합(R2DBC + MariaDB)이 Batch Insert를 제대로 지원하지 않는 건 아닐까?”
그래서 다시 열심히 구글링을 시작했다.
그 과정에서 아래 GitHub 이슈를 포함한 몇 가지 레퍼런스를 확인하게 되었다.
- spring-projects/spring-data-r2dbc의 Add support for batch insert (#259) GitHub
이 이슈에서는,
- DatabaseClient를 이용해 Batch Insert를 하고 싶다는 요청이 있었고,
- 작성자가 “워크어라운드로 Statement를 직접 써보긴 했지만, Connection을 직접 관리해야 해서 불편하다”고 적고 있다. GitHub
- 라벨이 status: pending-design-work로 달려 있으며,
“아직 정식으로 설계·지원되지 않은 기능”이라는 상태로 유지되고 있었다. GitHub
이걸 보고 정리한 결론은 단순했다.
“내가 API를 ‘틀리게’ 쓰고 있어서가 아니라,
Spring Data R2DBC 자체가 아직 원하는 형태의 Batch Insert를 공식 지원하지 않는 상황에 가깝다.”
즉, 프레임워크에 “알아서 Batch 처리해달라”고 맡기는 전략은 이 조합에서는 한계가 있다는 걸 받아들이게 되었다.
3-4. 방향 전환: 프레임워크에 맡기지 말고, 쿼리 패턴을 내가 설계하자
여기서부터 접근이 바뀌었다.
- “고수준 API를 더 파보자” → “실제 DB로 나가는 쿼리 패턴 자체를 바꾸자”
- 드라이버·프레임워크에 기대지 않고,
내가 원하는 형태의 INSERT 쿼리를 만들어서 한 번에 보내는 쪽으로 방향을 틀었다.
이때 떠올린 전통적인 RDB 패턴이 바로 다음과 같은 아주 기본적인 다중 VALUES Bulk Insert였다.
INSERT INTO key_table (col1, col2, col3) VALUES
('a1', 1, '2025-11-26'),
('a2', 2, '2025-11-27'),
... ,
('aN', N, '2025-12-01');
4. 해결 방향: 다중 VALUES 기반 Bulk Insert 유틸리티
위 패턴을 활용하면,
- 쿼리 호출 횟수 감소 → 네트워크 왕복 감소
- 쿼리 파싱/실행 계획 수립 횟수 감소
- 한 번의 트랜잭션 안에서 더 많은 row 처리
라는 장점이 있다.
그래서 R2DBC 환경에서도 이 형태를 쓰기 위해, 그리고 동료도 편히 쓸 수 있도록
다중 VALUES 기반 Bulk Insert SQL을 만들어주는 유틸리티를 직접 설계했다.
5. Bulk Insert 유틸리티 설계
코드를 블로그에 그대로 공개하는 것은 문제가 있어, 아주 단순화한 유틸리티를 활용한 예시는 다음과 같다.
5-1. BatchQuery 생성 예제
StringBuilder sqlSb = queryGenerator.getInitSql(ProjectUsers.class, true);
sqlSb.append(queryGenerator.generateBatchQuery(users, createId, ProjectUsers.class));
return databaseClient.sql(sqlSb.toString())
.fetch()
.all()
.map(row -> {
log.info("row is {}", row);
return ProjectUsers.builder().build();
});
⚠️ Security Note: SQL Injection 방어 이 방식은 Raw SQL을 생성하므로 SQL Injection에 취약할 수 있다. 따라서 이 유틸리티를 범용적으로 공개하지 않고, 엄격한 Type Check가 선행된 내부 로직에만 제한적으로 사용하도록 통제했다. 외부 사용자 입력을 직접 바인딩해야 한다면 ESAPI나 엄격한 정규식 검증을 거치도록 설계해야 하므로 주의가 필요하다.
6. 실제 적용 결과: 10,000건 기준 13분 → 34초
이 Bulk Insert 유틸리티는 실제로 차량 키 관리 솔루션의 대칭 키 생성 배치 작업에 적용했다.
최초 구현에서는 대칭 키 10,000개를 생성·저장하는 데 약 13분(780초)이 걸렸지만,
Bulk Insert 유틸리티 적용 후에는 10,000개 기준으로 환산하면 약 32초가 소요됐으며,
대략 23배 수준의 성능을 확보할 수 있었다.

7. 개발 편의성과 성능관점에서의 트레이드오프
내가 구현한 Bulk Insert 유틸리티는 Java Reflection을 사용하여 쿼리를 동적으로 생성했다.
이는 매번 개발자가 직접 Query를 작성하지 않고, 유지보수성을 높이기 위함이었다.
물론, 일반적으로 Reflection은 정적 코드보다 호출 비용이 높다고 알려져 있다.
하지만 이 기능의 핵심 병목(Bottleneck)은 CPU 연산이 아니라, DB와의 네트워크 통신(I/O)에 있었다.
실제로 10만 건의 데이터를 처리할 때 DB I/O가 수 초(Sec) 단위로 발생하는 반면, Reflection을 통한 문자열 생성은 밀리초(ms) 단위에 불과했다.
따라서 코드의 복잡도를 높이는 캐싱 로직 추가보다는, 유지보수성과 범용성을 챙기는 방향으로 구현했다.
'java,springboot' 카테고리의 다른 글
| 대시보드 대용량 데이터 처리(feat.웹 압축 기술) (1) | 2025.12.06 |
|---|---|
| WebFlux 환경에서 레거시 Logging 시스템을 AOP 기반으로 리팩토링한 기록 (0) | 2025.04.13 |
| [Java] Java에서 JSON 다루기(mapper, converter) (0) | 2024.04.04 |
| [Springboot] Reactive Redis 총 정리(config, generic, test) (1) | 2024.02.06 |
| try-with-resources 사용법 및 주의점 (1) | 2023.12.05 |