문제
- 기능: 엑셀로 장비 매핑 정보 일괄 업로드
- 수백 행 → 정상 동작
-
실제 현장 데이터 수만 행 → 배치 INSERT에서 터짐
- 구현: MyBatis
<foreach>멀티 로우 INSERT - 행이 늘수록 SQL 하나의 바인드 파라미터 개수도 선형 증가
- 그 개수에 상한 존재
원인
- PostgreSQL 확장 쿼리 프로토콜의 Bind 메시지 → 파라미터 개수를 int16으로 표현
- 부호 없는 16비트, 최대 65,535개
-
서버 설정으로 늘릴 수 있는 값이 아님 — 와이어 프로토콜 자체의 구조적 한계
- 한도가 걸리는 지점 — 행 수가 아니라 파라미터 수
파라미터 총수 = 행 수 × 행당 파라미터 수
- 같은 65,535라도 행당 컬럼 수에 따라 허용 행 수가 완전히 달라짐
- 따라서 “배치 사이즈 1000” 같은 고정 매직넘버는 답이 될 수 없음
- 어떤 쿼리에서는 과하게 보수적, 어떤 쿼리에서는 여전히 터짐
어떻게 고쳤나
- 각 배치 쿼리마다 행당 파라미터 수를 세고, 거기서 청크 크기를 역산
| 배치 쿼리 | 행당 파라미터 | 청크 크기 | 청크당 파라미터 |
|---|---|---|---|
| 이력 INSERT | 7 | 8,000 | 56,000 |
| 장비-고객 UPDATE | 2 | 20,000 | 40,000 |
| 장비-측정기 UPDATE | 4 | 10,000 | 40,000 |
- 전부 65,535 아래에 여유를 두고 떨어짐
- 딱 맞춘 값 65,535 ÷ 7 = 9,362 대신 8,000으로 끊은 건 의도적
-
이유: 나중에 컬럼이 하나 추가돼도 즉시 터지지 않을 마진
- 청킹 — 헬퍼 하나로 공통화
/**
* PostgreSQL PreparedStatement 파라미터 한도(65,535) 초과 방지용 청킹 헬퍼.
*/
private <T> List<List<T>> partition(List<T> list, int chunkSize) { ... }
- 호출부
private void insertMeterCustomerHistoryInChunks(List<Map<String, Object>> histories) {
if (histories == null || histories.isEmpty()) return;
for (List<Map<String, Object>> chunk : partition(histories, 8_000)) {
mappingDao.insertMeterCustomerHistoryBatch(chunk);
}
}
헬퍼의 Javadoc에 한도 숫자를 적어둔 게 이 변경에서 제일 중요한 부분일지도 모른다. 나중에 이 코드를 보는 사람이 “왜 8,000이지?”라고 물었을 때 답이 코드 옆에 있어야 하기 때문이다.
IN 절도 같은 문제를 갖는다
- 파라미터를 쓰는 건 INSERT/UPDATE만이 아님
-
WHERE id IN (...)조회도 원소마다 파라미터 하나씩 소비 - 같은 커밋에서 조회 쪽도 500개 단위로 청킹
int chunkSize = 500;
- 여기만 500이라는 훨씬 작은 값
- 근거가 다르기 때문
-
IN절은 파라미터 한도보다 플래너 비용이 먼저 문제 - 원소 수천 개 → 실행 계획 수립 자체가 무거워짐
- 옵티마이저가 인덱스 스캔 대신 해시 조인·시퀀셜 스캔으로 도망가기 쉬움
-
즉 프로토콜 한도가 아니라 성능 특성에서 나온 숫자
- 정리: 같은 “청킹”이나 상한의 근거가 다르고, 따라서 숫자도 다름
남는 교훈
배치 크기는 상수가 아니라 계산 결과다.
청크 크기 = (한도 ÷ 행당 파라미터 수) 에서 마진을 뺀 값
-
이 식을 모른 채 1000 같은 숫자를 넣을 때의 두 가지 오류
- 행당 파라미터 2개인 쿼리 → 필요 이상으로 왕복을 늘려 느려짐
- 행당 파라미터 70개인 쿼리 → 1000행에서 그대로 한도 초과
- 우연히 안 터지고 있을 뿐
그리고 이 종류의 버그는 테스트 데이터 규모가 작으면 절대 안 보인다.
- 개발용 샘플 엑셀 50행 → 파라미터 350개
- 한도의 0.5%
- 프로덕션에서 처음 만나게 되는 전형적인 형태
대량 업로드 기능을 만들 때는 실제 최대 규모에 가까운 데이터로 한 번은 돌려봐야 한다.
- JDBC 드라이버 버전 업그레이드로 해결되지 않음 — 프로토콜에 int16으로 박힌 값
- 우회는 청킹뿐
- 수십만 행을 자주 밀어넣어야 한다면
COPY검토