LabHub
배우기 러닝패스 코스

Apache Spark — The answer to a slow job is in the plan and the event log

Don't leave the schema to inference, and isolate broken rows instead of dropping them

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

CSV 에는 타입이 없어서 누군가 스키마를 정해야 한다. 추론은 자료를 한 번 더 읽고, 값 하나에 칼럼 전체의 타입이 흔들린다. 스키마를 직접 주고, 깨진 줄은 PERMISSIVE 모드와 손상 칼럼으로 버리지 말고 가려내서 몇 줄이 왜 깨졌는지 숫자로 남기는 것이 기본기다.

Concept map: 추론은 자료를 한 번 더 읽고, 값 하나에 칼럼 전체의 타입이 흔들린다. · 버리지 말고 가려내서 · PERMISSIVE · DROPMALFORMED

왜 스키마가 문제가 되나

Parquet 같은 형식은 파일 안에 스키마를 담고 있다. CSV 는 쉼표로 나뉜 글자일 뿐이다. 2 가 정수인지, 2026-01-03 10:00:00 이 타임스탬프인지는 파일이 말해 주지 않는다. 그래서 Spark 가 CSV 를 읽을 때는 둘 중 하나를 해야 한다. 사람이 스키마를 주거나, Spark 가 자료를 보고 짐작하거나.

짐작에는 값이 있다. CSV 데이터 소스 문서inferSchema 의 기본값이 false 이고, 켜면 "자료를 한 번 더 훑어야 한다" 고 적는다. 추론에 쓰는 비율 samplingRatio 의 기본값은 1.0 — 전부다. 협력사가 매일 보내는 10GB 파일이라면 매일 10GB 를 한 번 더 읽는 셈이다.

더 큰 문제는 결과가 자료에 따라 흔들린다는 것이다. 추론은 칼럼의 모든 값을 담을 수 있는 타입을 고른다. 수량 칸에 어느 날 two 라는 글자가 한 줄 섞이면 그 칼럼은 정수가 아니라 문자열로 추론된다. 이 실습의 협력사 파일이 정확히 그렇다. 추론에 맡기면 qty 와 order_ts 가 둘 다 string 으로 나온다. 어제까지 돌던 sum("qty") 가 오늘 이상한 값을 내는데, 코드는 한 글자도 바뀌지 않았다.

어떻게 동작하나

PySpark 의 csv 문서는 전체를 한 번 더 읽지 않으려면 추론을 끄거나 스키마를 직접 주라고 권하고, 스키마를 StructType 뿐 아니라 DDL 문자열로도 받는다.

ddl = ("order_id STRING, customer_id STRING, product_id STRING, qty INT, "
       "order_ts TIMESTAMP, status STRING, channel STRING, _corrupt_record STRING")
df = (spark.read
      .option("header", True)
      .option("mode", "PERMISSIVE")             # 기본값이지만 적어 둔다
      .schema(ddl)
      .csv("/data/partner/orders.csv"))
bad = df.where(F.col("_corrupt_record").isNotNull())

스키마를 주면 Spark 는 그 스키마대로 값을 바꿔 보다가 바꿀 수 없는 줄을 만난다. 그 줄을 어떻게 할지 정하는 것이 mode 옵션이다. 문서가 정의하는 세 가지는 이렇다.

손상 칼럼의 이름은 columnNameOfCorruptRecord 옵션으로 바꿀 수 있고, 안 주면 설정 spark.sql.columnNameOfCorruptRecord 의 기본값 _corrupt_record 를 쓴다.

같은 CSV 네 줄을 세 가지 모드로 읽은 그림. 수량 칸에 two 가 든 둘째 줄을 PERMISSIVE 는 수량만 null 로 두고 원문을 손상 칼럼에 담아 네 줄 모두 내놓고, DROPMALFORMED 는 그 줄을 조용히 빼서 세 줄을 내놓고, FAILFAST 는 그 줄에서 MALFORMED_RECORD_IN_PARSING 으로 멈춘다. 아래에는 count 만 하면 파싱을 건너뛰어 세 모드 모두 네 줄이 나온다는 함정이 적혀 있다

FAILFAST 로 실패하면 바깥 예외는 파일을 읽다 실패했다는 FAILED_READ_FILE 이고, 그 원인으로 MALFORMED_RECORD_IN_PARSING 이 붙는다. 이 오류의 설명문은 깨진 레코드를 null 로 처리하고 싶으면 mode 를 PERMISSIVE 로 두라고 안내한다.

count 가 거짓말을 하는 이유

여기가 이 모듈에서 가장 많이 틀리는 곳이다. 같은 문서의 mode 설명에는 짧은 경고가 붙어 있다. 칼럼 가지치기 아래에서 CSV 는 필요한 칼럼만 파싱하려 하므로, 어떤 줄이 손상으로 잡히는지는 요구된 칼럼 집합에 따라 달라진다. 이 동작은 spark.sql.csv.parser.columnPruning.enabled 로 조절하고 기본으로 켜져 있다.

count() 에는 칼럼이 하나도 필요 없다. 그래서 파서는 qty 를 정수로 바꿔 보지도 않고 줄 수만 센다. DROPMALFORMED 로 읽고 세면 깨진 줄을 빼지 않은 숫자가 나오고, FAILFAST 로 읽고 세면 아무 일 없이 끝난다. 실습의 6,000줄짜리 파일에서도 두 모드 모두 6,000 이 나온다. 모든 칼럼을 써야 하는 쓰기를 해야 비로소 깨진 줄이 빠진 5,560 줄이 나온다. "FAILFAST 로 돌렸는데 통과했으니 파일이 깨끗하다" 는 결론은 무엇을 요구했는지와 함께가 아니면 성립하지 않는다.

문서와 동작이 어긋나는 곳

문서의 PERMISSIVE 설명은 칸 수가 스키마보다 모자라거나 넘치는 줄을 CSV 에서는 손상 레코드로 보지 않는다고 적는다. 모자라면 남는 칸을 null 로 채우고, 넘치면 남는 토큰을 버린다는 것이다. 그런데 이 파드의 Spark 4.2 에서 손상 칼럼을 둔 스키마로 읽어 보면 칸 수가 어긋난 줄도 원문이 손상 칼럼에 담겨 나온다. 실습 파일에서 그렇게 가려지는 줄이 모두 440 줄이다. 판이 바뀌면 이런 세부가 움직인다. 문서는 출발점이고, 판정은 직접 센 숫자로 한다.

현장에서 만나는 모습

조용히 버리는 모드가 가장 위험하다. DROPMALFORMED 는 편해 보이지만 몇 줄을 왜 버렸는지 아무 기록도 남기지 않는다. 협력사가 형식을 바꿔 절반이 깨져도 파이프라인은 초록불이다. 현장에서는 PERMISSIVE 로 읽고, 손상 칼럼이 채워진 줄을 격리 테이블로 따로 쓰고, 그 수를 지표로 남긴다.

파싱이 통과해도 업무 규칙은 따로다. 수량 -3 은 정수로 멀쩡히 바뀐다. 파서는 형식만 볼 뿐 의미는 모른다. 음수 수량이나 미래 날짜처럼 업무상 불가능한 값은 파싱 뒤에 별도 조건으로 가려내 같은 격리 테이블로 보낸다. 깨끗한 결과는 두 관문을 모두 지난 줄이다.

머리줄은 기본적으로 무시된다. 문서에서 enforceSchema 의 기본값은 true 이고, 이때 지정하거나 추론한 스키마를 강제로 적용하면서 CSV 의 머리줄은 무시한다. 협력사가 칼럼 순서를 바꿔 보내면 이름이 아니라 위치로 들어가 조용히 틀린 값이 된다. 문서도 틀린 결과를 피하려면 이 옵션을 끄라고 권한다.

실무에서 진짜 중요한 것

다음 실습에서 할 것

협력사가 보낸 주문 CSV 6,000 줄을 DDL 스키마로 읽는다. 먼저 추론에 맡겼을 때 qty 와 order_ts 가 문자열로 추론되는 것을 확인하고, PERMISSIVE 와 손상 칼럼으로 깨진 줄을 가려내 몇 줄인지 센다. 같은 파일을 DROPMALFORMED 로 읽어 세기만 한 값과 쓰고 다시 센 값이 어떻게 다른지 보고, FAILFAST 로 쓰다가 실패한 예외에서 파싱 오류 조건 이름을 받아 본다. 줄을 쉼표로 나눈 칸 수별로 세어 깨진 줄 가운데 칸 수가 틀린 것이 얼마인지 견주고, 마지막으로 파싱 실패와 빈 수량·음수 수량 같은 업무 규칙 위반을 이유 칼럼과 함께 따로 격리해 깨끗한 결과를 만든다.