LabHub
学习 学习路径 课程

Apache Spark — 느린 잡의 답은 실행 계획과 이벤트 로그에 있다 · 스키마와 깨진 줄 · 讲解

스키마를 추론에 맡기지 않고, 깨진 줄은 버리지 말고 가려낸다

在 LabHub 中继续学习

한 줄 요약

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

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

왜 스키마가 문제가 되나

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

짐작에는 값이 있다. [CSV 데이터 소스 문서](https://spark.apache.org/docs/4.2.0/sql-data-sources-csv.html#data-source-option)는 inferSchema 의 기본값이 false 이고, 켜면 "자료를 한 번 더 훑어야 한다" 고 적는다. 추론에 쓰는 비율 samplingRatio 의 기본값은 1.0 — 전부다. 협력사가 매일 보내는 10GB 파일이라면 매일 10GB 를 한 번 더 읽는 셈이다.

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

어떻게 동작하나

[PySpark 의 csv 문서](https://spark.apache.org/docs/4.2.0/api/python/reference/pyspark.sql/api/pyspark.sql.DataFrameReader.csv.html)는 전체를 한 번 더 읽지 않으려면 추론을 끄거나 스키마를 직접 주라고 권하고, 스키마를 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 옵션으로 바꿀 수 있고, 안 주면 [설정](https://spark.apache.org/docs/4.2.0/configuration.html#runtime-sql-configuration) spark.sql.columnNameOfCorruptRecord 의 기본값 _corrupt_record 를 쓴다.

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

FAILFAST 로 실패하면 바깥 예외는 파일을 읽다 실패했다는 [FAILED_READ_FILE](https://spark.apache.org/docs/4.2.0/sql-error-conditions.html#failed-read-file) 이고, 그 원인으로 [MALFORMED_RECORD_IN_PARSING](https://spark.apache.org/docs/4.2.0/sql-error-conditions.html#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 로 쓰다가 실패한 예외에서 파싱 오류 조건 이름을 받아 본다. 줄을 쉼표로 나눈 칸 수별로 세어 깨진 줄 가운데 칸 수가 틀린 것이 얼마인지 견주고, 마지막으로 파싱 실패와 빈 수량·음수 수량 같은 업무 규칙 위반을 이유 칼럼과 함께 따로 격리해 깨끗한 결과를 만든다.