REST API에서 쿼리 파라미터를 올바르게 인코딩하는 방법
REST API 쿼리 파라미터는 &, =, 공백, 유니코드 같은 특수 문자를 안전하게 포함하기 위해 RFC 3986에 따라 percent-encoding되어야 합니다. JavaScript에서는 encodeURIComponent(), Python에서는 urllib.parse.quote(), Java에서는 URLEncoder.encode()를 사용하세요.
쿼리 파라미터에 인코딩이 필요한 이유
REST API의 쿼리 문자열은 키-값 쌍을 &로 구분하고 키와 값을 =로 구분하는 구조화된 형식을 사용합니다. 파라미터 값에 이러한 구분자 문자나 #, +, 공백 같은 다른 예약 문자가 포함되면 쿼리 문자열의 구조가 깨지고 서버는 잘못된 데이터를 받게 됩니다.
다음 예시를 살펴봅시다. API에서 "salt & pepper"를 검색하고 싶다고 가정해 보겠습니다. 인코딩하지 않으면 /search?q=salt & pepper라는 URL은 서버에게 두 개의 파라미터, 즉 q=salt 와 (값이 없는) pepper가 있다고 알려주게 됩니다. 반면 올바르게 인코딩한 /search?q=salt%20%26%20pepper는 "salt & pepper"라는 값을 가진 하나의 파라미터 q를 정확하게 전달합니다.
정확성뿐만 아니라, 올바른 인코딩은 보안 취약점도 예방합니다. URL에 인코딩되지 않은 사용자 입력이 포함되면 인젝션 공격, 캐시 포이즈닝을 비롯한 여러 익스플로잇으로 이어질 수 있습니다. URL에 파라미터 값을 포함하기 전에 항상 인코딩하세요.
언어별 인코딩 방법
주요 프로그래밍 언어는 모두 URL 인코딩을 위한 내장 함수를 제공합니다. 가장 널리 쓰이는 언어들에서 쿼리 파라미터를 올바르게 인코딩하는 방법은 다음과 같습니다.
// JavaScript
// 개별 파라미터 값에는 encodeURIComponent()를 사용하세요
const query = 'price >= 100 & category = books';
const url = 'https://api.example.com/search?q=' + encodeURIComponent(query);
// "https://api.example.com/search?q=price%20%3E%3D%20100%20%26%20category%20%3D%20books"
// 또는 여러 파라미터에는 URLSearchParams를 사용하세요
const params = new URLSearchParams({
q: 'price >= 100 & category = books',
page: '1',
sort: 'price'
});
const url2 = 'https://api.example.com/search?' + params.toString();
# Python
from urllib.parse import urlencode, quote
# 여러 파라미터에는 urlencode 사용
params = {
'q': 'price >= 100 & category = books',
'page': 1,
'sort': 'price'
}
query_string = urlencode(params)
url = f'https://api.example.com/search?{query_string}'
# 단일 값에는 quote 사용
value = quote('price >= 100 & category = books', safe='')
url = f'https://api.example.com/search?q={value}'
// Java
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
String query = "price >= 100 & category = books";
String encoded = URLEncoder.encode(query, StandardCharsets.UTF_8);
// "price+%3E%3D+100+%26+category+%3D+books"
// 참고: URLEncoder는 공백을 +로 인코딩합니다 (폼 인코딩)
// RFC 3986 인코딩을 위해서는 +를 %20으로 치환하세요
String rfc3986 = encoded.replace("+", "%20");
String url = "https://api.example.com/search?q=" + rfc3986;
// C#
using System.Net;
using System.Web;
string query = "price >= 100 & category = books";
// Uri.EscapeDataString는 RFC 3986을 따릅니다
string encoded = Uri.EscapeDataString(query);
// "price%20%3E%3D%20100%20%26%20category%20%3D%20books"
// HttpUtility.UrlEncode는 폼 인코딩을 사용합니다 (공백을 +로)
string formEncoded = HttpUtility.UrlEncode(query);
// "price+%3e%3d+100+%26+category+%3d+books"
// Go
package main
import (
"fmt"
"net/url"
)
func main() {
// url.QueryEscape는 쿼리 문자열용으로 인코딩합니다 (공백을 +로)
encoded := url.QueryEscape("price >= 100 & category = books")
// "price+%3E%3D+100+%26+category+%3D+books"
// url.PathEscape는 경로 세그먼트용으로 인코딩합니다 (공백을 %20으로)
pathEncoded := url.PathEscape("my file name.pdf")
// "my%20file%20name.pdf"
// 쿼리 문자열을 구성할 때는 url.Values 사용
params := url.Values{}
params.Set("q", "price >= 100 & category = books")
params.Set("page", "1")
queryString := params.Encode()
fmt.Println(queryString)
}
배열과 중첩 객체 인코딩
REST API는 배열이나 중첩 객체를 쿼리 파라미터로 받아야 하는 경우가 많습니다. 이러한 복합 타입을 인코딩하는 단일 표준은 없기 때문에, API마다 서로 다른 관례를 사용합니다.
// 키 반복 (가장 널리 지원됨)
// GET /api/items?tag=javascript&tag=python&tag=go
const params = new URLSearchParams();
['javascript', 'python', 'go'].forEach(tag => params.append('tag', tag));
// 대괄호 표기법 (PHP, Rails, qs를 사용하는 Express에서 일반적)
// GET /api/items?tags[]=javascript&tags[]=python&tags[]=go
// 직접 인코딩해야 합니다:
const tags = ['javascript', 'python', 'go'];
const qs = tags.map(t => 'tags[]=' + encodeURIComponent(t)).join('&');
// 쉼표 구분 (간단하지만 제약이 있음)
// GET /api/items?tags=javascript,python,go
const tagList = encodeURIComponent('javascript,python,go');
// 중첩 객체 (대괄호 표기법)
// GET /api/search?filter[status]=active&filter[min_price]=10
// Python에서:
params = {
'filter[status]': 'active',
'filter[min_price]': '10'
}
기대하는 형식은 항상 API 문서에서 확인하세요. 새 API를 설계할 때는 키 반복 방식이 여러 프로그래밍 언어와 HTTP 라이브러리에서 가장 이식성이 높습니다.
서버 측 디코딩 모범 사례
서버 측에서 들어오는 요청을 처리할 때, 대부분의 웹 프레임워크는 쿼리 파라미터를 자동으로 디코딩해 줍니다. 그러나 견고한 서버 측 처리를 위해 염두에 두어야 할 중요한 고려 사항들이 있습니다.
- 대부분의 프레임워크(Express, Django, Spring, ASP.NET)는 쿼리 파라미터를 자동으로 디코딩합니다. 이를 수동으로 다시 디코딩하지 마세요. 그렇지 않으면 이중 디코딩이 발생합니다.
- 디코딩된 값을 사용하기 전에 기대하는 타입과 범위에 맞는지 검증하세요.
- 웹 서버 수준에서 최대 URL 길이 제한을 설정하세요(일반적으로 2048자 또는 8192자).
- 콘텐츠 타입에 따라
+가 공백을 의미할 수도, 문자 그대로의 플러스 기호를 의미할 수도 있는 경우를 처리하세요. - 디버깅을 위해 원본(인코딩된) URL을 로깅하세요. 디코딩된 값은 로그에서 오해를 불러일으킬 수 있습니다.
// Express.js - 파라미터는 자동으로 디코딩됩니다
app.get('/search', (req, res) => {
const query = req.query.q; // 이미 디코딩됨
// 다음처럼 하지 마세요: decodeURIComponent(req.query.q)
// 입력값 검증
if (typeof query !== 'string' || query.length > 200) {
return res.status(400).json({ error: 'Invalid query parameter' });
}
// 디코딩된 값을 안전하게 사용
const results = search(query);
res.json(results);
});
보안 고려 사항
올바른 쿼리 파라미터 인코딩은 API 보안의 핵심 요소입니다. 반드시 따라야 할 가장 중요한 보안 관행들은 다음과 같습니다.
- 클라이언트 측 인코딩을 절대 신뢰하지 마세요. 클라이언트가 올바르게 인코딩했더라도, 항상 서버에서 디코딩된 파라미터를 검증하고 정제하세요.
- 이중 인코딩 공격에 주의하세요. 공격자는 한 번만 디코딩하는 입력 필터를 우회하기 위해 악의적인 문자를 두 번 인코딩할 수 있습니다. 보안 필터가 완전히 디코딩된 값을 보도록 하세요.
- 경로 순회(path traversal)를 방지하세요. 경로 파라미터에 포함된
%2e%2e%2f(../) 같은 인코딩된 시퀀스는 의도한 디렉터리 외부의 파일에 접근하는 데 악용될 수 있습니다. 파일 작업 전에 경로를 디코딩하고 검증하세요. - 파라미터화된 쿼리를 사용하세요. URL 디코딩 이후에도 사용자 입력을 SQL이나 NoSQL 쿼리에 직접 연결하지 마세요. 파라미터화된 쿼리나 ORM을 사용하세요.
- URL 길이를 제한하세요. 지나치게 긴 쿼리 문자열은 서비스 거부(denial-of-service)를 유발할 수 있습니다. 합리적인 길이를 초과하는 URL을 거부하도록 웹 서버를 구성하세요.
- 출력도 인코딩하세요. 쿼리 파라미터 값을 HTML 응답에 다시 반영할 때는 크로스 사이트 스크립팅(XSS) 공격을 방지하기 위해 HTML 엔티티 인코딩을 적용하세요.