C++를 그대로 붙였더니 C와 공백 두 개가 됐습니다

검색어를 URL 뒤에 붙이는 코드를 점검하려고 q=와 입력 문자열을 단순히 더한 경우를 만들었습니다. Python의 폼 방식 쿼리 파서에서 q=C++를 읽으면 값은 C 뒤에 공백 두 개가 붙은 문자열입니다. 이번 실험은 실제 검색 사이트의 장애를 재현한 것이 아니라, 문자열 생성과 파싱을 같은 프로세스에서 대조한 격리 실험입니다.
2026년 10월 23일 C++, A B, 서울&부산, a=b, 100%, 빈 문자열을 입력했습니다. 각 값에 대해 원시 연결 쿼리, 그 파싱 결과, urlencode 쿼리, 다시 읽은 결과를 보관했습니다. 여섯 입력 모두 올바르게 인코딩한 경로에서는 입력과 결과가 일치한다는 단언문을 실행했습니다.
공백의 +와 문자 그대로의 +를 나눕니다
폼 방식 쿼리에서 공백을 나타내는 +와 검색어의 더하기 기호는 다른 역할입니다. urlencode는 C++의 두 기호를 %2B%2B로 바꿉니다. 반대로 A B는 A+B로 만들어도 같은 파서로 읽으면 원래 공백으로 돌아옵니다. 핵심은 모든 +를 금지하는 것이 아니라 값의 문자와 쿼리 문법을 구분하는 것입니다.
이미 인코딩한 C%2B%2B를 다시 원본 값처럼 urlencode에 넣으면 쿼리는 q=C%252B%252B가 됩니다. 이를 한 번 파싱한 값은 C++가 아니라 C%2B%2B입니다. 원본 문자열에서 시작한 왕복 검사와 완성된 URL 조각을 다시 처리하는 경우를 구분해야 합니다. 인코딩을 여러 번 하면 더 안전해지는 것이 아니라 다른 값을 만들 수 있습니다.
| 원본 입력 | 직접 붙인 쿼리 | 원시 파싱값 | urlencode 쿼리 | 되돌린 값 |
|---|---|---|---|---|
| 'C++' | q=C++ | {"q":["C␠␠"]} | q=C%2B%2B | {"q":["C++"]} |
| 'A B' | q=A B | {"q":["A␠B"]} | q=A+B | {"q":["A␠B"]} |
| '서울&부산' | q=서울&부산 | {"q":["서울"],"부산":[""]} | q=%EC%84%9C%EC%9A%B8%26%EB%B6%80%EC%82%B0 | {"q":["서울&부산"]} |
| 'a=b' | q=a=b | {"q":["a=b"]} | q=a%3Db | {"q":["a=b"]} |
| '100%' | q=100% | {"q":["100%"]} | q=100%25 | {"q":["100%"]} |
| '' | q= | {"q":[""]} | q= | {"q":[""]} |
서울&부산은 두 검색어가 아니라 한 값일 수 있습니다
직접 붙인 q=서울&부산에서 &는 값 안의 글자가 아니라 필드 구분자로 해석됩니다. 보관한 결과에는 q 값이 서울로 잘리고 부산이라는 빈 필드가 별도로 생깁니다. urlencode에 원본 문자열을 전달하면 &를 값의 일부로 보존할 수 있습니다. 사람이 읽는 검색 문장을 먼저 쪼개지 말고 데이터와 문법을 나누어 조합해야 하는 이유입니다.
a=b도 같이 넣었습니다. parse_qs는 이 예제에서 첫 = 뒤를 값으로 읽으므로 모든 기호 입력이 같은 방식으로 깨지는 것은 아닙니다. 문제가 없는 한 사례만 보고 수동 연결을 일반화하지 않고, +·&·=·%·한글을 각각 포함한 작은 회귀 입력을 유지하는 편이 낫습니다.
빈 문자열과 누락, 0은 서로 다른 상태입니다

parse_qs의 기본 설정은 q=의 빈값을 결과에서 제외합니다. keep_blank_values=True를 지정하면 q의 값 목록에 빈 문자열이 남습니다. q 자체가 없는 요청, q가 있지만 빈 요청, q=0인 요청은 필터와 검색 기능에서 다른 뜻을 가질 수 있으므로 서버가 어떤 계약을 쓸지 먼저 정해야 합니다.
같은 키가 반복되는 q=C%2B%2B&q=A+B도 읽었습니다. 결과는 값 두 개를 가진 목록입니다. 딕셔너리 하나에 마지막 값만 저장하는 다른 처리로 바꾸면 이 정보가 사라질 수 있습니다. 어느 값을 선택할지는 서비스 정책이며, 파서가 돌려준 목록을 확인하는 것과 최종 검색 조건을 결정하는 것은 별도 단계입니다.
쿼리 성공을 URL 경로 전체의 성공으로 확대하지 않습니다
여기서 확인한 것은 application/x-www-form-urlencoded 방식의 쿼리입니다. 파일 경로나 URL의 path 전체에 같은 + 규칙을 적용했다는 뜻이 아닙니다. 경로 조합, 프록시의 디코딩, 웹 프레임워크의 중복 키 처리와 브라우저 주소창은 이번 실행 범위에 포함하지 않았습니다.
실서비스에서는 브라우저가 보낸 URL, 서버가 받은 원시 쿼리, 애플리케이션이 파싱한 값 중 어느 단계에서 바뀌는지 좁혀 확인합니다. 운영 로그에 사용자의 검색어 전체를 남겨야 한다는 의미는 아닙니다. 먼저 이번처럼 개인정보 없는 고정 합성 입력으로 재현하고, 실제 서비스의 프레임워크 문서와 비교할 수 있습니다.
같은 입력이 돌아오는지 검증하는 작은 검사
재현 코드는 네트워크 없이 여섯 입력의 결과와 Python 버전을 JSON으로 저장합니다. 내려받은 이름이 lab-query-encoding.py.txt라면 마지막 .txt를 제거해 코드를 읽고, python3 lab-query-encoding.py --output my-query-results.json으로 실행할 수 있습니다. 이 출력 옵션은 같은 이름의 파일이 이미 있으면 중단합니다. 셸의 > 출력으로 기존 검산 파일을 덮어쓰지 않도록 실행 방법을 구분하세요.
원본 문자열을 문자열 조각으로 붙이지 않고 구조화된 매개변수로 넘기는 것을 기본 선택으로 삼겠습니다. 다만 빈값을 남길지, 중복 키를 허용할지까지 인코더가 대신 정해 주지는 않습니다. 이 두 가지 계약을 명시하고 원본과 되돌린 값의 일치 검사를 함께 두어야 같은 오류를 다시 찾기 쉽습니다.
python3 lab-query-encoding.py --output my-query-results.json내 검색어는 연결 전후에 같은 값일까요?
원본 검색어를 직접 붙이는 경우와 브라우저 URLSearchParams로 생성하는 경우를 비교합니다. 입력은 브라우저에서만 처리하며 실제 검색 요청을 보내지 않습니다.
C%2B%2B를 원본으로 넣으면 그 문자열 자체를 보존합니다. 원래 의도가 C++였는지는 왕복 일치만으로 알 수 없습니다. 위 입력에는 인코딩 전의 값을 넣으세요.
이미 받은 쿼리의 빈값·중복 키 읽기
JavaScript URLSearchParams의 결과입니다. 본문 실험의 Python parse_qs는 기본적으로 빈 값을 버릴 수 있어 keep_blank_values 등의 조건을 맞춰 비교해야 합니다. URL Standard의 API 정의 · 기존 Python 실험 원자료
주소 전체를 받았는데 첫 q만 사라진다면
https://example.invalid/search?q=C%2B%2B&q=&debug=0#result를 쿼리 전용 생성자에 그대로 넣으면 주소 앞부분이 이름에 섞입니다. 전체 주소 모드에서는 new URL(input).searchParams로 읽어 q의 두 값 ["C++", ""]를 유지하고, 경로 /search와 fragment #result를 분리합니다. debug 값은 0이며 fragment가 붙지 않습니다.
반대로 쿼리 문자열 모드의 q=a#b는 그 API에 준 문자열 자체이므로 값이 a#b입니다. 주소로 해석한 결과와 같다고 가정하지 않습니다. 전체 주소를 쿼리 모드에 붙이면 이제 모드 변경 안내가 나오며 잘못된 결과를 메모로 저장하지 않습니다. 실제 주소 방문이나 서버 응답 검사는 하지 않습니다.
편집자 의견
검색어 조합은 원본 값을 urlencode에 넘기고 되돌린 결과까지 비교하는 방식을 권합니다. C++ 한 건의 모양보다 여섯 경계 입력의 왕복 일치가 더 명확한 근거입니다.
빈값과 중복 키는 서비스 계약에 맞게 결정해야 합니다. 이 로컬 결과로 브라우저·프록시·모든 프레임워크의 처리를 검증했다고 보지는 않습니다.
판단 근거: 공백의 +와 문자 그대로의 +를 나눕니다
의견 추가 2026-10-23 · 본문 자료를 바탕으로 한 편집 판단
실험 환경과 원자료
관찰 시각: 2026-10-23T05:11:04+00:00
python: 3.10.12 · method: 로컬 표준 라이브러리 · 외부 요청 없음
여섯 원본 입력을 직접 연결·urlencode한 뒤 parse_qs로 읽고 왕복 일치 단언문 실행
확인하지 않은 범위: Python form-style query parsing, no browser, web server or external service tested.
Python 3 표준 라이브러리만 사용합니다. 네트워크 요청은 없습니다. 표준 출력 또는 --output으로 지정한 새 파일에만 결과를 씁니다. --output의 기존 파일은 덮어쓰지 않습니다.
python3 lab-query-encoding.py --output new-observations.json참고한 공식 문서
원고 수정: 2026-10-24 · 정정 요청
이 글의 보완 기록
- · 여섯 입력의 실제 직렬화·파싱과 빈값·중복 키 결과를 신규 공개했습니다.
- · 본문 근거에 대한 편집자의 판단과 결론이 달라지는 조건을 추가했습니다. 기존 관찰일·공식 자료 확인일은 유지했습니다.
- · 글별 적용 사례와 반례를 보강했습니다. 기존 관찰일과 원자료 확인일은 유지했습니다.