주요 컨텐츠로 이동

PySpark의 매개변수화 쿼리 (Parameterized queries)

Matthew Powers
Daniel Tenedorio
Hyukjin Kwon
이 포스트 공유하기

(번역: Leah Seo)  Original Blog Post 

PySpark는 언제나 데이터 쿼리를 위한 훌륭한 SQL 및 Python API를 제공해 왔습니다. Databricks Runtime 12.1 과 Apache Spark 3.4부터 매개변수화 쿼리는 Python 프로그래밍 패러다임을 사용하여 SQL로 데이터를 쿼리하는 안전하고 효과적인 방법을 지원합니다.

이 글에서는 PySpark로 매개변수화 쿼리를 만드는 방법과 이러한 디자인 패턴이 어떠한 경우에 적합한지 설명합니다.

매개변수는 Spark 코드를 더 쉽게 재사용하고 테스트하는 데 유용합니다. 또한 좋은 코딩 관행을 장려합니다. 이 글에서는 PySpark 쿼리에 매개변수화 쿼리를 사용하는 두 가지 방법에 대해 설명합니다:

  1. PySpark 사용자 지정 문자열 서식 (custom string formatting)
  2. 매개변수 마커 (Parameter markers)

두 가지 유형의 PySpark 매개변수화 쿼리를 이용하는 방법을 살펴보고 내장된 기능이 다른 대안보다 나은 이유에 대해 살펴보겠습니다.

매개변수화 쿼리의 장점

매개변수화 쿼리는 "반복 방지"(DRY) 패턴을 권장하고, 단위 테스트를 더 쉽게 만들며, SQL을 손쉽게 재사용 할 수 있도록 합니다. 또한 보안 취약점을 야기할 수 있는 SQL 인젝션 공격을 방지합니다.

유사한 쿼리를 작성할 때 대량의 SQL을 복사하여 붙여넣고 싶은 유혹에 빠질 수 있습니다. 매개변수화 쿼리는 패턴을 추상화하고 DRY 패턴으로 코드를 작성하는 것을 권장합니다.

매개변수화 쿼리는 테스트하기도 더 쉽습니다. 쿼리에 매개변수를 사용하면 프로덕션 배포도 쉬워지고 데이터셋을 테스트 하기도 수월해 집니다. 

반면에 Python f-스트링으로 SQL 쿼리를 수동으로 매개변수화하는 것은 좋지 않은 대안입니다. 다음과 같은 단점을 고려하세요:

  1. Python f-스트링은 SQL 인젝션 공격으로부터 보호해주지 않습니다.
  2. Python f-스트링은 데이터 프레임, 컬럼, 특수 문자와 같은 Python 네이티브 객체를 인식하지 못합니다.

매개변수 마커를 사용하여 쿼리를 매개변수화하는 방법은 SQL 인젝션 취약점으로부터 코드를 보호하고, 스트링 형식을 가진 일반적인 PySpark 인스턴스의 자동 형변환을 지원합니다. 이 방법을 살펴 봅시다.

PySpark 사용자 지정 문자열 서식을 사용하는 매개변수화 쿼리

열이 9개인 h20_1e9 라는 데이터 테이블이 있다고 가정해 보겠습니다:

+-----+-----+------------+---+---+-----+---+---+---------+
|  id1|  id2|         id3|id4|id5|  id6| v1| v2|       v3|
+-----+-----+------------+---+---+-----+---+---+---------+
|id008|id052|id0000073659| 84| 89|82005|  5| 11|64.785802|
|id079|id037|id0000041462|  4| 35|28153|  1|  1|28.732545|
|id098|id031|id0000027269| 27| 38|13508|  5|  2|59.867875|
+-----+-----+------------+---+---+-----+---+---+---------+

다음 SQL 쿼리를 매개변수화 하려고 합니다:

SELECT id1, SUM(v1) AS v1 
FROM h20_1e9 
WHERE id1 = "id089"
GROUP BY id1

다른 id1 값으로 이 쿼리를 쉽게 실행하고 싶습니다. 다음은 다른 id1 값으로 쿼리를 매개변수화하여 실행하는 방법입니다.

query = """SELECT id1, SUM(v1) AS v1 
FROM h20_1e9 
WHERE id1 = {id1_val} 
GROUP BY id1"""

spark.sql(query, id1_val="id016").show()

+-----+------+
|  id1|    v1|
+-----+------+
|id016|298268|
+-----+------+

이제 다른 인수(argument)를 사용하여 쿼리를 다시 실행합니다:

spark.sql(query, id1_val="id018").show()

+-----+------+
|  id1|    v1|
+-----+------+
|id089|300446|
+-----+------+

또한 PySpark 문자열 서식을 사용하면 임시 뷰(temporary view)를 명시적으로 정의하지 않고도 데이터프레임에서 직접 SQL 쿼리를 실행할 수 있습니다.

person_df 라는 데이터프레임이 있다고 가정해 보겠습니다:

+---------+--------+
|firstname| country|
+---------+--------+
|    frank|     usa|
|   sourav|   india|
|    rahul|   india|
|      sim|buglaria|
+---------+--------+

SQL로 데이터프레임을 쿼리하는 방법은 다음과 같습니다.

spark.sql(
    "select country, count(*) as num_ppl from {person_df} group by country",
    person_df=person_df,
).show()

+--------+-------+
| country|num_ppl|
+--------+-------+
|     usa|      1|
|   india|      2|
|bulgaria|      1|
+--------+-------+

임시 뷰를 수동으로 등록할 필요 없이 SQL 구문을 사용하여 데이터프레임에서 쿼리를 실행하는 것은 매우 멋진 일입니다!

이제 매개변수 마커의 인수를 사용하여 쿼리를 매개변수화하는 방법을 살펴보겠습니다.

매개변수 마커를 사용한 매개변수화 쿼리

매개변수 마커를 사용하는 매개변수화 SQL 쿼리를 작성할 때 인수의 딕셔너리(dictionary)를 사용할  수도 있습니다.

some_purchases 라는 이름의 뷰가 있다고 가정해 봅시다:

+-------+------+-------------+
|   item|amount|purchase_date|
+-------+------+-------------+
|  socks|  7.55|   2022-05-15|
|handbag| 49.99|   2022-05-16|
| shorts|  25.0|   2023-01-05|
+-------+------+-------------+

다음은 명명된 매개변수 마커(named parameter markers)를 사용하여 매개변수화된 쿼리를 만들어 특정 항목에 지출된 총 금액을 계산하는 방법입니다.

query = "SELECT item, sum(amount) from some_purchases group by item having item = :item"	

양말(socks)에 지출한 총 금액을 계산합니다.

spark.sql(
    query,
    args={"item": "socks"},
).show()

+-----+-----------+
| item|sum(amount)|
+-----+-----------+
|socks|      32.55|
+-----+-----------+

명명되지 않은 매개변수 마커(unnamed parameter markers)를 사용하여 쿼리를 매개변수화할 수도 있습니다. 자세한 내용은 여기를 참조하세요.

Apache Spark는 매개변수 마커를 소독(sanitize)하므로 이 매개변수화 접근 방식은 SQL 인젝션 공격으로부터도 사용자를 보호합니다.

PySpark 이 매개변수화된 쿼리를 소독하는 방법

다음은 Spark가 명명된 매개변수화 쿼리를 소독하는 방법에 대한 개략적인 설명입니다:

  • SQL 쿼리가 선택적 키/값 매개변수 목록과 함께 도착합니다.
  • Apache Spark는 SQL 쿼리를 파싱하고 매개변수 참조(reference)를 해당 파싱 트리 노드로 대체합니다.
  • 분석 중에 카탈리스트 규칙이 실행되어 이러한 참조를 매개변수에서 제공된 매개변수 값으로 대체합니다.
  • 이 접근 방식은 리터럴 값만 지원하기 때문에 SQL 인젝션 공격으로부터 보호합니다. 일반 문자열 보간(interpolation)은 SQL 문자열에 치환을 적용하는데, 이 전략은 의도한 리터럴 값 이외의 SQL 구문이 문자열에 포함된 경우 공격에 취약할 수 있습니다.

앞서 언급했듯이 PySpark에서 지원하는 매개변수화 쿼리에는 두 가지 유형이 있습니다:

  • PEP 3101을 기반으로 {} 구문을 사용하는 클라이언트 측 매개변수화 (우리는 이를 사용자 지정 문자열 서식이라고 부릅니다).
  • 명명된 매개변수 마커(named parameter) 또는 명명되지 않은 매개변수 마커(unnamed parameter markers)를 사용하는 서버 측 매개변수화.

{} 구문은 클라이언트 측에서 SQL 쿼리에 문자열 대체를 수행하여 사용 편의성과 프로그래밍 편의성을 높입니다. 그러나 쿼리 텍스트가 Spark 서버로 전송되기 전에 대체되기 때문에 SQL 인젝션 공격으로부터 보호하지는 못합니다.

매개변수화는 sql() API의 args 인수를 사용하여 SQL 텍스트와 매개변수를 서버에 별도로 전달합니다. SQL 텍스트는 매개변수 자리표시자로 구문 분석되어, 분석된 쿼리 트리의 args에 지정된 매개변수 값으로 대체됩니다.

서버 측 매개변수화 쿼리에는 명명된 매개변수 마커와 명명되지 않은 매개변수 마커의 두 가지 유형이 있습니다. 명명된 매개변수 마커는 자리 표시자에 :<param_name> 구문을 사용합니다. 명명되지 않은 매개변수 마커를 사용하는 방법에 대한 자세한 내용은 문서를 참조하세요.

매개변수화 쿼리 vs. 문자열 보간(interpolation)

일반적인 Python 문자열 보간을 사용하여 쿼리를 매개변수화할 수도 있지만 이 방법은 그리 편리하지 않습니다.

이전 쿼리를 Python f- 문자열로 매개변수화하는 방법은 다음과 같습니다:

some_df.createOrReplaceTempView("whatever")
the_date = "2021-01-01"
min_value = "4.0"
table_name = "whatever"

query = f"""SELECT * from {table_name}
WHERE the_date > '{the_date}' AND number > {min_value}"""
spark.sql(query).show()

다음과 같은 이유로 이 방법은 좋지 않습니다:

  • 임시 뷰를 만들어야 합니다.
  • 날짜를 Python 날짜가 아닌 문자열로 표현해야 합니다.
  • 쿼리에서 날짜를 작은따옴표로 묶어 SQL 문자열의 형식을 올바르게 지정해야 합니다.
  • 이렇게 하면 SQL 인젝션 공격으로부터 보호되지 않습니다.

요약하면, 내장된 쿼리 매개변수화 기능이 문자열 보간보다 더 안전하고 효과적입니다.

결론

PySpark 매개변수화 쿼리는 익숙한 SQL 구문으로 깔끔한 코드를 작성할 수 있는 새로운 기능을 제공합니다. SQL로 Spark 데이터프레임을 쿼리할 때 편리합니다. 매개변수화 쿼리를 사용하면 부동 소수점 값, 문자열, 날짜, 날짜/시간과 같은 일반적인 Python 데이터 유형을 사용할 수 있으며, 이 데이터 유형은 내부에서 자동으로 SQL 값으로 변환됩니다. 이러한 방식으로 이제 일반적인 Python 관용구를 활용하여 멋진 코드를 작성할 수 있습니다.

PySpark 매개변수화 쿼리를 바로 활용해 보세요. 고품질 코드 베이스의 이점을 즉시 누릴 수 있습니다.

Databricks 무료로 시작하기

관련 포스트

모든 엔지니어링 블로그 포스트 보기