파이썬 requests URL 인코딩 오류, params와 urlencode를 구분하는 법

일반적인 쿼리 값은 원문 그대로 params에 넘기고, 이미 퍼센트 인코딩된 값을 다시 params에 넣지 않는 것이 출발점입니다. requests는 params로 쿼리 문자열을 구성하므로 한글·공백·+·&·=가 들어간 값도 원문과 실제 전송 URL을 나누어 확인해야 합니다. 서비스키처럼 이미 인코딩된 값을 받는 API는 계약이 다를 수 있으므로, 인코딩 결과만 보고 인증 성공까지 단정하지 않는 것이 안전합니다.

먼저 구분할 것: 원문 값인가, 완성된 쿼리인가

문제는 대개 값의 상태와 전달 방식이 섞일 때 생깁니다. 예를 들어 검색어가 python + api라면 이것은 아직 값입니다. 반면 python+%2B+api처럼 퍼센트 인코딩된 문자열은 이미 한 차례 쿼리 값으로 변환된 상태입니다.

일반적인 요청은 원문 값을 params에 맡기는 쪽이 읽기 쉽고 확인도 간단합니다.

import requests

params = {
    "query": "python + api",
    "page": 1,
}

response = requests.get("https://example.com/search", params=params)
print(response.request.url)

Requests는 params를 사용해 쿼리 문자열을 구성합니다. params 입력에는 dict뿐 아니라 tuple 목록이나 bytes도 사용할 수 있고, 값이 None인 dict 항목은 쿼리에 추가되지 않습니다. 선택적 조건을 만들 때는 이 동작이 의도한 것인지도 확인해야 합니다.

반대로 이미 인코딩된 값을 다시 params에 넣으면 % 자체가 값의 문자로 취급될 수 있습니다. 그 결과 %26이 %2526처럼 보이는 이중 인코딩이 나타날 수 있습니다. %25가 보인다는 사실은 중요한 단서지만, 그것만으로 해당 API의 인증 실패 원인이나 응답 성공 여부를 확정할 수는 없습니다.

params와 urlencode를 선택하는 기준

판단 순서는 다음처럼 잡으면 됩니다.

  1. 값이 아직 원문이면 먼저 params에 전달합니다.
  2. 서버 계약상 쿼리 문자열을 직접 만들어야 하는 경우에만 urlencode를 검토합니다.
  3. 이미 완성한 URL을 사용한다면 같은 키를 params에 다시 넣지 않습니다.
  4. 요청을 보낸 뒤 실제 준비된 URL을 출력해 원문 의도와 비교합니다.

urlencode는 mapping 또는 두 요소로 된 tuple sequence를 퍼센트 인코딩된 ASCII 쿼리 문자열로 바꿉니다. 기본 quote_plus 동작에서는 공백이 +로 표현되고, 원래 값에 있던 +는 이스케이프됩니다.

from urllib.parse import urlencode

query = urlencode({"query": "python + api"})
url = "https://example.com/search?" + query
print(url)

이 방식은 쿼리 문자열 자체가 필요한 상황에는 유용하지만, 그 결과를 다시 같은 키의 params 값으로 넣는 방식은 피해야 합니다. 이미 만들어진 URL을 보낼지, 원문 값을 params로 보낼지 한 경로만 선택해야 합니다.

공백, +, &, =를 실제 값으로 확인하기

쿼리 값에 구분 문자가 들어가면 눈으로 URL을 읽을 때 오해하기 쉽습니다. 원문 공백의 +와 원문 플러스 기호의 %2B는 구분해서 봐야 합니다. 값에 들어 있던 &와 =가 %26, %3D로 보이는 것도 같은 맥락입니다.

다음처럼 특수문자를 포함한 원문 값을 params에 넣고 준비된 URL을 확인해 보십시오.

import requests

params = {
    "text": "A+B & C=D",
    "language": "한글",
}

request = requests.Request(
    "GET",
    "https://example.com/search",
    params=params,
).prepare()

print(request.url)

여기서 확인할 것은 URL에 특정 모양이 반드시 나와야 한다는 규칙이 아니라, 값의 경계가 보존되었는지입니다. &가 값 안에 있었는데 쿼리 항목을 나누는 구분자로 풀렸다면 전달 방식을 다시 살펴야 합니다. 공백과 +도 원문과 전송 표현을 나란히 놓고 확인하는 편이 안전합니다.

실제 전송 URL을 어디서 확인할까

요청을 보내기 전에는 PreparedRequest를 만들어 URL을 확인할 수 있습니다. 이미 요청을 보냈다면 response.request.url에서도 준비된 요청의 URL을 확인할 수 있습니다.

import requests

params = {"service": "원문 값", "q": "A+B & C=D"}
request = requests.Request(
    "GET",
    "https://example.com/data",
    params=params,
).prepare()

print("prepared:", request.url)

# 실제 호출을 하는 경우
# response = requests.get("https://example.com/data", params=params)
# print("sent:", response.request.url)

운영 로그에 서비스키를 그대로 남기는 것은 피해야 합니다. 확인이 필요하면 민감한 값은 일부를 가린 뒤 쿼리 키와 인코딩 형태만 비교하는 방식이 적절합니다. 이 글의 예시는 URL 구성과 검증 순서를 설명하기 위한 것이며, 특정 API의 인증 방식이나 성공 응답을 대신 판정하지 않습니다.

여러 값을 보내야 할 때

같은 키에 여러 값을 전달해야 한다면 값의 목록이나 키와 값으로 된 tuple 목록을 사용할 수 있습니다.

import requests

params = {
    "tag": ["python", "automation"],
}

request = requests.Request(
    "GET",
    "https://example.com/search",
    params=params,
).prepare()

print(request.url)

urlencode를 직접 사용하는 경우에는 doseq=True가 sequence 값을 같은 키의 여러 항목으로 확장합니다.

from urllib.parse import urlencode

query = urlencode(
    [("tag", ["python", "automation"])],
    doseq=True,
)
print(query)

두 방식 중 무엇을 쓸지는 결과를 어디에 넘길지로 결정하면 됩니다. Requests 요청의 쿼리라면 원문 구조를 params로 전달하는 편이 관리하기 쉽고, 별도의 쿼리 문자열이 필요한 계약이라면 urlencode 결과를 한 번만 사용합니다.

quote를 쓸 때의 주의점

urlencode의 기본 quote_plus와 달리 quote는 기본적으로 슬래시를 안전 문자로 취급합니다. 값에 슬래시가 있고 계약상 인코딩이 필요하다면 urlencode의 safe 인자와 함께 어떤 문자를 보존할지 확인해야 합니다.

from urllib.parse import quote

value = "path/with spaces"
print(quote(value))

quote와 quote_plus를 섞어 쓰면서 결과만 맞추려 하면, 값의 의미보다 표현에 끌려가기 쉽습니다. 먼저 서버가 기대하는 값의 계약을 확인하고, 그 계약이 완성된 쿼리 문자열을 요구할 때만 직접 인코딩을 선택하십시오.

서비스키 오류를 만났을 때의 점검 순서

공공데이터 API처럼 서비스키가 포함된 요청에서는 다음 순서로 범위를 좁히는 편이 좋습니다.

  1. 전달받은 키가 원문인지 이미 퍼센트 인코딩된 문자열인지 확인합니다.
  2. API 문서가 요구하는 키 형식을 확인합니다. 서비스마다 허용 형식이 다를 수 있습니다.
  3. 키를 params에 넣었다면 response.request.url 또는 준비된 요청 URL을 확인합니다.
  4. %25가 생겼다면 이미 인코딩된 값을 다시 인코딩했는지 살핍니다.
  5. URL이 의도대로 보이더라도 응답 본문과 API의 오류 설명을 별도로 확인합니다.

인코딩은 URL을 구성하는 문제이고, 인증 성공은 해당 API의 계약과 서버 응답에 관한 문제입니다. 따라서 URL이 예쁘게 보인다는 이유만으로 서비스키가 유효하다고 판단해서는 안 됩니다.

정리

원문 쿼리 값은 params에 맡기고, 직접 만든 쿼리 문자열은 다시 params 값으로 감싸지 않는 것이 핵심입니다. %25, %2B, %26, %3D 같은 표현은 진단에 도움을 주지만, 실제로 무엇을 의미하는지는 원문 값과 준비된 URL을 함께 비교해야 합니다.

다음으로 확인할 질문은 두 가지입니다. 요청 URL이 아니라 본문으로 보내야 하는 값이라면 data와 json 중 무엇이 맞는지, 그리고 HTTP 상태 코드가 성공이어도 응답 본문에 API 오류가 들어 있는지 어떻게 검증할지입니다.

출처

Leave a Comment