Blog
pysparksparkjdbcrdbmsparalleldata-engineering

PySpark JDBC 대량 읽기·쓰기 — 단일 커넥션 병목과 운영 DB 보호

Spark 로 RDBMS 에서 수억 행을 읽을 때 단일 커넥션으로 한 익스큐터만 일하는 병목, partitionColumn 병렬 읽기, fetchsize·batchsize 튜닝, 그리고 운영 DB 를 마비시키지 않는 쓰기 전략을 정리합니다.

Data Dynamics2026년 6월 5일10 min read

Spark 로 PostgreSQL·MySQL·Oracle 같은 RDBMS 에서 데이터를 읽고 쓰는 일은 흔합니다. 그런데 순진하게 spark.read.jdbc(...) 를 호출하면 단 하나의 커넥션으로 한 익스큐터만 일하고, 나머지 클러스터는 놀게 됩니다. 수억 행을 단일 스레드로 읽으니 몇 시간이 걸립니다. 반대로 병렬도를 무작정 높이면 운영 DB 가 커넥션 폭주로 마비됩니다.

이 글은 JDBC 대량 읽기를 병렬화하는 법, 핵심 튜닝 파라미터, 그리고 운영 DB 를 보호하면서 쓰는 전략을 정리합니다.

1. 기본 동작의 함정 — 단일 파티션

# 위험: 파티션 옵션 없이 읽으면 단일 커넥션 = 단일 태스크
df = (spark.read
    .format("jdbc")
    .option("url", "jdbc:postgresql://db:5432/app")
    .option("dbtable", "events")
    .option("user", "trino").option("password", "...")
    .load())
# → 익스큐터 1대만 일함, 1억 행을 한 스레드로 ☠️

파티션 옵션을 안 주면 Spark 는 쿼리 결과를 하나의 파티션으로 가져옵니다. 클러스터가 아무리 커도 소용없습니다.

2. 병렬 읽기 — partitionColumn

읽기를 병렬화하려면 Spark 에게 "어떤 컬럼으로, 몇 개로 쪼개서" 읽을지 알려줘야 합니다. Spark 는 이 정보로 범위를 나눈 여러 쿼리를 동시에 던집니다.

df = (spark.read
    .format("jdbc")
    .option("url", "jdbc:postgresql://db:5432/app")
    .option("dbtable", "events")
    .option("user", "trino").option("password", "...")
    # 병렬 읽기 4종 세트
    .option("partitionColumn", "id")       # 분할 기준 (숫자/날짜, 인덱스 있는 컬럼)
    .option("lowerBound", "1")             # 최솟값
    .option("upperBound", "100000000")     # 최댓값
    .option("numPartitions", "32")         # 병렬도 = 동시 커넥션 수
    .load())

Spark 는 이를 이렇게 변환합니다:

파티션 1: WHERE id >= 1          AND id < 3125000
파티션 2: WHERE id >= 3125000    AND id < 6250000
...
파티션 32: WHERE id >= 96875000  AND id <= 100000000
→ 32개 커넥션이 동시에 각 범위를 읽음
옵션의미주의
partitionColumn분할 기준 컬럼인덱스 있는 숫자/날짜
lowerBound/upperBound범위 경계데이터 분포와 맞아야
numPartitions병렬도(=동시 커넥션)DB 부하와 직결

핵심: lowerBound/upperBound필터가 아니라 범위 분할용입니다. 이 범위를 벗어난 데이터도 첫/마지막 파티션에 포함됩니다. 다만 경계가 실제 분포와 크게 어긋나면 파티션이 불균등해져 스큐가 납니다.

3. 분할 컬럼 스큐 — 가장 흔한 실패

partitionColumn 값이 고르게 분포하지 않으면, 어떤 파티션은 텅 비고 어떤 파티션은 거대해집니다.

id 가 1~100M 인데 실제로는 99%가 1~1M 에 몰려 있다면
→ 첫 파티션만 거대, 나머지 31개는 거의 빈 채로 끝남 → 병렬화 무의미

대응:

  • 균등 분포하는 컬럼을 고르세요(auto-increment ID, 균등한 타임스탬프).
  • 분포가 치우치면 lowerBound/upperBound 를 실제 분포에 맞추거나, 해시 기반 분할 컬럼을 미리 만듭니다.
  • 분포를 모르면 DB 에서 분위수를 먼저 조회해 경계를 정합니다.

4. fetchsize — 행을 한 번에 얼마나 가져오나

fetchSize 는 JDBC 드라이버가 한 번의 네트워크 왕복에 가져오는 행 수입니다. 기본값이 작으면(드라이버마다 다름, 일부는 10) 왕복이 폭증해 느립니다.

.option("fetchsize", "10000")    # 한 번에 1만 행씩 가져옴
fetchsize효과
너무 작음네트워크 왕복 폭증 → 느림
너무 큼익스큐터 메모리 압박
적정(1k~10k)왕복·메모리 균형

PostgreSQL 은 fetchsize 를 설정해야 커서 기반 스트리밍이 동작합니다(안 하면 전체 결과를 클라이언트 메모리로). MySQL 은 별도 옵션이 필요할 수 있습니다. 드라이버별 동작을 확인하세요.

5. 운영 DB 보호 — 병렬도의 두 얼굴

numPartitions 는 양날의 검입니다. 높이면 빠르지만, 그만큼 운영 DB 에 동시 커넥션·쿼리 부하가 갑니다.

numPartitions = 64  → 운영 DB 에 64개 동시 풀스캔 쿼리
→ 운영 트랜잭션과 자원 경합, 커넥션 풀 고갈, DB 마비 가능

운영 DB 보호 원칙:

원칙방법
읽기 복제본 사용운영 primary 대신 read replica 연결
병렬도 제한numPartitions 를 DB 가 감당할 수준으로
한가한 시간대야간 배치로 대량 추출
커넥션 한도 인지DB 의 max_connections 대비 여유
쿼리 타임아웃DB 측 statement_timeout 으로 폭주 차단

철칙: 대량 JDBC 추출은 운영 primary 가 아니라 read replica 를 가리키세요. 분석 풀스캔이 운영 트랜잭션을 방해하면 안 됩니다. (Trino 페더레이션에서도 같은 원칙 — 별도 글 "Trino 페더레이션 실전".)

6. 대량 쓰기 — batchsize 와 멱등성

Spark → RDBMS 쓰기도 튜닝이 필요합니다.

(df.write
    .format("jdbc")
    .option("url", "...")
    .option("dbtable", "target")
    .option("batchsize", "5000")           # INSERT 배치 크기
    .option("numPartitions", "16")         # 동시 쓰기 커넥션
    .option("isolationLevel", "NONE")      # 대량 적재 시 (신중히)
    .mode("append")
    .save())
옵션역할
batchsize배치 INSERT 행 수(왕복↓)
numPartitions동시 쓰기 커넥션
truncateoverwrite 시 DROP 대신 TRUNCATE

쓰기의 함정:

  • 멱등성: append 재실행은 중복을 만듭니다. 재처리가 있으면 staging 테이블 적재 후 DB 쪽 MERGE/UPSERT 로, 또는 키 충돌 처리를 설계하세요.
  • 운영 부하: 대량 쓰기도 DB 에 부담. 병렬도·배치 크기를 조절.
  • 대안: 대량을 RDBMS 에 직접 쓰기보다, Lakehouse(Iceberg/Delta)에 쓰고 DB 는 작은 결과만 받는 구조가 보통 낫습니다.

7. 패턴 선택

상황권장
대량 읽기(전체 추출)partitionColumn 병렬 + replica + 야간
증분 읽기WHERE 로 변경분만(dbtable 에 서브쿼리)
대량 결과 쓰기Lakehouse 에 쓰기(DB 직접 쓰기 지양)
소량 결과 쓰기batchsize 적정 + 멱등 설계
반복 조인 분석DB 스냅샷을 Lakehouse 로 적재 후 분석
# 증분 읽기: dbtable 에 서브쿼리로 변경분만
.option("dbtable", "(SELECT * FROM events WHERE updated_at >= '2026-06-01') AS t")

8. 정리

영역핵심
병렬 읽기partitionColumn + bounds + numPartitions
분할 컬럼균등 분포·인덱스 있는 컬럼
fetchsize네트워크 왕복 줄이기
DB 보호read replica, 병렬도 제한, 야간
쓰기batchsize, 멱등 설계, Lakehouse 우선

JDBC 대량 I/O 의 핵심은 두 가지 균형입니다 — 충분히 병렬화해 클러스터를 활용하되, 운영 DB 를 마비시키지 않는 선을 지키는 것. partitionColumn 으로 병렬 읽기를 켜고 fetchsize 로 왕복을 줄이되, read replica 를 쓰고 병렬도를 DB 가 감당할 수준으로 제한하세요. 그리고 대량 결과는 RDBMS 에 직접 쓰기보다 Lakehouse 에 적재하는 것이, 운영 DB 와 분석을 분리하는 가장 건강한 아키텍처입니다.


이 글은 Spark 3.5 기준으로 작성되었습니다. RDBMS-Lakehouse 연동이나 대량 추출 파이프라인 설계가 필요하시면 언제든 문의해 주세요.

— Data Dynamics 엔지니어링 팀