ANSI 모드와 캐스트 에러 — 크래시와 조용한 데이터 손실 중 무엇을 고를 것인가
PySpark Tutorial: Build a Real Pipeline and Read the Spark UI — Darshil Parmar PySpark Tutorial: Build a Real Pipeline and Read the Spark UI — "Eight rows out of 3,384 will kill the whole job" 구간 (8~10페이지)
이 모듈을 다 읽으면
- CSV가 스키마를 갖지 않기 때문에 printSchema() 결과가 전부 string으로 나온다는 사실을 설명할 수 있다
- Spark 4에서 ANSI 모드가 기본 활성화되어 잘못된 캐스팅이 조용한 NULL 대신 즉시 예외로 드러나는 이유를 Spark 3 이전 동작과 비교해 설명할 수 있다
- try_cast가 에러의 '탈출구'처럼 보이지만 실제로는 크래시를 조용한 매출 누락으로 바꿀 뿐이라는 점과, inferSchema=True가 이 문제를 해결해주지 못하는 이유를 설명할 수 있다
raw_orders.csv를 읽으면 다섯 컬럼이 전부 string으로 잡히는데, 이는 CSV 자체에 타입 정보가 없기 때문이다. amount 컬럼에 섞인 "1,104.25" 같은 천 단위 구분자 때문에 sum()을 호출하면 CAST_INVALID_INPUT 예외가 발생한다. Spark 4는 ANSI 모드가 기본값이라 이런 캐스트 실패를 조용히 NULL로 넘기지 않고 즉시 예외로 드러내는데, 이는 버그가 아니라 설계다. 에러 메시지가 제안하는 try_cast로 우회하면 예외 없이 실행되지만, 실패한 8개 행이 NULL로 바뀌어 합계 집계에서 조용히 빠지고 총합이 정확히 그만큼 부족해진다. inferSchema=True 옵션도 구원투수가 아니다 — 이미 쉼표로 오염된 문자열에서는 추론도 string으로 남긴다.
CSV는 타입이 없다 — printSchema가 전부 string인 이유
raw.printSchema()를 실행하면 order_id, customer_id, order_ts, amount, status 다섯 컬럼이 모두 string(nullable=true)으로 나온다. CSV 포맷 자체가 어떤 컬럼이 숫자이고 어떤 컬럼이 날짜인지 알려주는 메타데이터를 갖고 있지 않기 때문에, header=True만 준 상태에서는 모든 값을 문자열로 읽는 것이 정상 동작이다. 문제는 amount 컬럼의 일부 행에 "1,104.25"처럼 천 단위 구분 쉼표가 섞여 있다는 점인데, 이 상태에서 raw.agg(F.sum("amount"))를 호출하면 [CAST_INVALID_INPUT] 예외가 발생하며 잡이 그대로 죽는다. 전체 3,384행 중 단 8행(0.2%)이 원인이라는 점이 이 사례의 핵심이다 — 극히 일부의 오염된 값이 전체 집계를 멈춰 세운다.
핵심 포인트
- CSV는 스키마가 없는 포맷이라 header=True만으로는 모든 컬럼이 string으로 읽힌다
- amount 컬럼의 쉼표 포함 값 8건(0.2%)이 sum() 호출 시 CAST_INVALID_INPUT 예외를 일으킨다
- 적은 비율의 오염된 값이라도 캐스트가 실패하면 잡 전체가 멈출 수 있다
Spark 4의 ANSI 모드 — 조용한 NULL 대신 즉시 예외 — 정정 보강
이 캐스트 실패가 예외로 즉시 드러나는 이유는 Spark 4부터 spark.sql.ansi.enabled 기본값이 true로 바뀌었기 때문이다. Spark 3.x까지는 이 값이 기본 false였고, 표준(non-try) 캐스트가 실패하면 예외 없이 조용히 NULL을 반환했다. 즉 동일한 8개의 오염된 행이 Spark 3 이하에서는 아무 경고 없이 NULL로 바뀐 채 넘어갔을 것이고, 이 튜토리얼의 저자가 실제로 예외를 마주친 것은 Spark 4의 ANSI 기본 활성화 덕분이다. 이는 버그가 아니라 '데이터가 이상하면 빨리, 시끄럽게 실패시킨다'는 명시적 설계 방향의 결과다.
핵심 포인트
- spark.sql.ansi.enabled는 Spark 4부터 기본값이 true이며, Spark 3.x 이하는 기본 false였다
- ANSI 모드가 꺼져 있던 시절에는 동일한 캐스트 실패가 조용히 NULL을 반환했다
- Spark 4의 동작 변화는 결함이 아니라 오염된 데이터를 조기에 드러내려는 의도적 설계다
try_cast는 탈출구가 아니라 함정 — 크래시가 매출 누락보다 나은 이유
에러 메시지는 try_cast를 쓰라고 제안하지만, 그대로 적용하면 실패한 8개 행이 예외 대신 NULL이 되고 F.sum()은 NULL을 자동으로 건너뛴다. 결과적으로 잡은 에러 없이 '성공'하지만 합계는 정확히 그 8개 행의 금액만큼(약 18,649.00) 부족한 채로 나온다. 크래시하는 잡은 원인을 찾는 데 10분이 걸리지만, 조용히 매출이 누락된 채 성공한 잡은 그 오차가 분기 실적 보고서에 그대로 반영될 때까지 아무도 눈치채지 못할 수 있다. 근본적인 해법은 regexp_replace로 쉼표를 제거한 뒤 명시적으로 cast하는 것이다. inferSchema=True도 해결책이 아니다 — 파일을 두 번 읽어 order_ts는 정확히 타임스탬프로 추론하지만, amount는 쉼표 때문에 이미 파싱 불가능한 문자열이라 그대로 string으로 남는다. 스키마 추론은 이미 깨진 데이터에서 타입을 추측할 뿐, 데이터를 정제해주지는 않는다.
핵심 포인트
- try_cast는 캐스트 실패를 예외 대신 NULL로 바꾸고, sum()은 그 NULL을 조용히 건너뛴다
- 크래시하는 잡은 원인 파악이 빠르지만, 조용히 성공하는 잡은 누락된 금액을 아무도 알아채지 못하게 만든다
- inferSchema=True는 이미 쉼표로 오염된 문자열에서 타입을 추측할 뿐이라 amount는 여전히 string으로 남는다
- 근본 해법은 regexp_replace로 쉼표를 제거한 뒤 명시적으로 cast하는 것이다