Skip to content
Published on

HTTP QUERY 메소드 — REST의 오랜 딜레마를 푸는 새로운 동사

Authors

들어가며

최근 GeekNews(news.hada.io)와 해커뉴스(Hacker News)에서 HTTP QUERY 메소드에 관한 글이 크게 화제가 되었습니다. 특히 kreya.app 블로그의 "HTTP QUERY method" 글이 GeekNews 인기글로 올라오면서, 국내 개발자들 사이에서도 "드디어 검색 API를 제대로 설계할 동사가 생기는 건가"라는 반응이 쏟아졌습니다.

REST API를 한 번이라도 진지하게 설계해 본 사람이라면 누구나 겪었을 딜레마가 있습니다. 복잡한 검색 조건을 어떻게 서버로 보낼 것인가 하는 문제입니다. 필터가 단순하면 GET 쿼리 스트링으로 충분하지만, 중첩된 필터와 정렬, 페이지네이션, 집계 조건이 뒤섞인 복잡한 검색이라면 이야기가 달라집니다.

GET을 쓰자니 요청 본문(request body)을 보낼 수 없고, URL 길이 제한에 부딪힙니다. POST를 쓰자니 본문은 보낼 수 있지만 "검색"이라는 행위에 비해 의미론(semantics)이 전혀 맞지 않습니다. POST는 안전(safe)하지도, 멱등(idempotent)하지도, 캐시 가능(cacheable)하지도 않으니까요.

이 글에서는 이 오래된 딜레마의 본질을 짚고, IETF가 표준화를 진행 중인 HTTP QUERY 메소드가 어떻게 이 문제를 풀어내는지, 그리고 실무에서 지금 무엇을 고려해야 하는지 깊이 있게 살펴보겠습니다.


1. 문제의 본질 — GET과 POST 사이의 빈틈

1.1 GET의 한계

GET은 HTTP의 가장 기본적인 동사이며, 리소스를 조회하는 데 사용됩니다. GET의 핵심 속성은 다음과 같습니다.

  • 안전(Safe): 서버 상태를 변경하지 않습니다.
  • 멱등(Idempotent): 여러 번 호출해도 결과가 같습니다.
  • 캐시 가능(Cacheable): 응답을 캐시할 수 있습니다.

문제는 GET이 의미론적으로 본문을 갖지 않는다는 점입니다. RFC 9110은 GET 요청에 본문을 보내는 것에 대해 "정의된 의미가 없으며, 일부 구현은 이를 거부할 수 있다"고 명시합니다. 즉 GET 본문은 사실상 사용할 수 없습니다.

그래서 복잡한 검색 조건은 모두 URL 쿼리 스트링에 담아야 합니다. 여기서 세 가지 현실적인 문제가 발생합니다.

문제 영역구체적인 증상
URL 길이 제한브라우저와 프록시, 서버마다 한도가 다르며 보통 2KB에서 8KB 수준
인코딩 복잡성중첩 구조를 쿼리 스트링으로 표현하려면 직렬화 규칙이 지저분해짐
로그 노출검색어와 조건이 액세스 로그, 프록시 로그, 브라우저 히스토리에 남음

특히 URL 길이 제한은 표준으로 정해진 값이 아니라 구현 의존적입니다. 일부 오래된 프록시나 서버는 더 짧은 한도를 가질 수 있어, 긴 검색 조건은 예측 불가능하게 잘려나가거나 거부됩니다.

쿼리 스트링으로 중첩 구조를 표현하는 예를 보겠습니다.

GET /products?filter[category]=shoes&filter[price][gte]=100&filter[price][lte]=200&sort[]=-rating&sort[]=price&page[number]=2&page[size]=20

이 방식은 표준이 없어서 프레임워크마다 파싱 규칙이 다릅니다. 같은 의도를 JSON으로 표현하면 훨씬 명확합니다.

{
  "filter": {
    "category": "shoes",
    "price": { "gte": 100, "lte": 200 }
  },
  "sort": ["-rating", "price"],
  "page": { "number": 2, "size": 20 }
}

JSON 본문이 명백히 더 읽기 쉽고 구조적입니다. 하지만 GET으로는 이 본문을 보낼 수 없습니다.

1.2 POST의 의미론적 어긋남

그래서 대부분의 개발자는 POST로 도피합니다. POST는 본문을 자유롭게 보낼 수 있으니 위 JSON을 그대로 실어 보내면 됩니다.

POST /products/search HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "filter": { "category": "shoes", "price": { "gte": 100, "lte": 200 } },
  "sort": ["-rating", "price"],
  "page": { "number": 2, "size": 20 }
}

동작은 합니다. 하지만 의미론적으로 POST는 검색에 전혀 맞지 않는 동사입니다. POST의 표준 속성을 보겠습니다.

속성GETPOST검색에 필요한가
안전성있음없음필요함
멱등성있음없음필요함
캐시 가능성있음없음유용함

검색은 본질적으로 읽기 작업입니다. 서버 상태를 바꾸지 않고(안전), 몇 번을 호출해도 같은 결과를 돌려주며(멱등), 같은 조건이면 응답을 캐시할 수 있어야(캐시 가능) 합니다. 그런데 POST는 이 세 가지를 모두 보장하지 않습니다.

POST를 검색에 쓰면 다음과 같은 부작용이 생깁니다.

  • 중간 캐시(CDN, 리버스 프록시)가 응답을 캐시하지 못합니다.
  • 클라이언트 라이브러리나 프록시가 안전하게 재시도(retry)할 수 없습니다. POST 재시도는 자칫 부작용을 일으킬 수 있다고 가정하기 때문입니다.
  • 모니터링과 API 게이트웨이가 이 요청을 "쓰기"로 분류해 읽기 전용 정책(예: 읽기 복제본 라우팅)을 적용하지 못합니다.

요약하면, GET은 본문이 없고 POST는 의미가 틀렸습니다. 이 빈틈이 바로 QUERY 메소드가 메우려는 자리입니다.


2. HTTP QUERY 메소드란 무엇인가

2.1 한 줄 정의

HTTP QUERY 메소드는 요청 본문을 가질 수 있으면서, 안전하고 멱등한 새로운 HTTP 동사입니다. 쉽게 말해 "본문을 보낼 수 있는 GET"입니다.

IETF의 HTTP 워킹그룹(httpbis)이 "HTTP QUERY Method"라는 인터넷 드래프트(draft-ietf-httpbis-safe-method-w-body)로 표준화를 진행하고 있습니다. 드래프트 이름에 담긴 "safe-method-with-body(본문을 가진 안전한 메소드)"라는 표현이 이 동사의 본질을 정확히 요약합니다.

2.2 핵심 속성

QUERY 메소드의 의미론은 다음 세 가지로 정리됩니다.

속성QUERY의미
안전성있음서버 상태를 변경하지 않는 읽기 작업
멱등성있음여러 번 호출해도 동일한 결과를 보장
본문 허용있음요청 본문에 구조화된 검색 조건을 담을 수 있음

여기에 더해 응답은 적절한 조건이 갖춰지면 캐시할 수 있습니다. 이 점이 핵심입니다. QUERY는 본문을 갖는데도 캐시 가능성을 포기하지 않습니다.

세 동사를 한눈에 비교하면 다음과 같습니다.

항목GETPOSTQUERY
요청 본문사실상 불가가능가능
안전성있음없음있음
멱등성있음없음있음
캐시 가능성있음없음있음 (조건부)
검색 적합성본문 제약의미 어긋남의미가 정확히 맞음

2.3 요청과 응답의 모양

QUERY 요청은 GET처럼 안전하지만 POST처럼 본문을 실어 보냅니다. 앞서 본 상품 검색을 QUERY로 표현하면 다음과 같습니다.

QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{
  "filter": {
    "category": "shoes",
    "price": { "gte": 100, "lte": 200 }
  },
  "sort": ["-rating", "price"],
  "page": { "number": 2, "size": 20 }
}

서버는 일반적인 검색 결과를 반환합니다.

HTTP/1.1 200 OK
Content-Type: application/json

{
  "total": 142,
  "results": [
    { "id": "p-1001", "name": "Trail Runner", "price": 159 },
    { "id": "p-1002", "name": "City Walker", "price": 129 }
  ]
}

GET과 비교하면 동사만 바뀌었을 뿐 의미는 그대로 "조회"입니다. POST와 비교하면 본문은 똑같이 보내지만 안전성과 멱등성이라는 의미론적 보장이 추가됩니다.


3. QUERY 응답의 캐시 가능성

3.1 캐시 키의 문제

GET 응답을 캐시할 때 캐시 키는 보통 URL입니다. 같은 URL이면 같은 응답이라는 가정이죠. 그런데 QUERY는 검색 조건이 본문에 들어 있습니다. URL은 같고 본문만 다른 두 요청이 서로 다른 결과를 반환할 수 있다는 뜻입니다.

따라서 QUERY 응답을 캐시하려면 본문도 캐시 키에 포함해야 합니다. HTTP QUERY 드래프트는 이를 위한 메커니즘을 논의합니다. 핵심 아이디어는 서버가 응답에서 정규화된(canonical) 캐시 키 정보를 제공하고, 중간 캐시가 이를 활용하도록 하는 것입니다.

3.2 Content-Location을 통한 캐시 처리

한 가지 접근은 서버가 응답에 Content-Location 헤더로 "이 쿼리에 대응하는 정규 URL"을 알려주는 방식입니다. 그러면 동일한 쿼리에 대해서는 그 URL을 캐시 키로 사용할 수 있습니다.

QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json

{ "filter": { "category": "shoes" }, "page": { "size": 20 } }

응답에서 정규 위치를 제시합니다.

HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /products?category=shoes&size=20
Cache-Control: max-age=60

{ "total": 88, "results": [] }

이렇게 하면 본문 기반 검색의 표현력과 GET 기반 캐싱의 장점을 동시에 취할 수 있습니다. 다만 이 부분은 드래프트에서 여전히 논의가 진행 중인 영역이므로, 구현 시에는 최신 드래프트의 권고를 따라야 합니다.

3.3 캐시 동작 비교

시나리오GETPOSTQUERY
브라우저 캐시기본 동작캐시 안 됨가능 (구현 필요)
CDN/리버스 프록시광범위 지원보통 미지원점진적 지원 예상
캐시 키URL캐시 안 함URL 더하기 본문

4. 지금까지의 우회책들

QUERY가 표준화되기 전까지 업계는 다양한 우회책으로 이 문제를 다뤄왔습니다. 각각의 장단점을 살펴보면 QUERY가 왜 필요한지 더 분명해집니다.

4.1 검색 전용 POST 엔드포인트

가장 흔한 방식입니다. 검색을 위한 별도의 POST 엔드포인트를 만드는 것이죠. Elasticsearch의 검색 API가 대표적인 예입니다.

POST /products/_search HTTP/1.1
Host: search.example.com
Content-Type: application/json

{
  "query": {
    "bool": {
      "must": [{ "match": { "category": "shoes" } }],
      "filter": [{ "range": { "price": { "gte": 100, "lte": 200 } } }]
    }
  },
  "sort": [{ "rating": "desc" }],
  "from": 20,
  "size": 20
}

Elasticsearch는 GET과 POST를 모두 허용하지만, 본문을 안전하게 보내기 위해 사실상 POST가 표준 관행이 되었습니다. 동작에는 문제가 없지만 앞서 본 의미론적 한계를 그대로 떠안습니다. 검색이 "쓰기"로 분류되고 캐시되지 않습니다.

URL 끝에 붙는 _search 같은 동사형 경로(action path)는 RESTful 설계 원칙과도 어긋납니다. REST는 명사형 리소스를 지향하는데, _search는 명백한 동사이기 때문입니다.

4.2 거대한 GET 쿼리 스트링

또 다른 방식은 모든 조건을 GET 쿼리 스트링에 욱여넣는 것입니다. 단순한 검색에는 잘 동작하지만, 조건이 복잡해질수록 URL이 통제 불능으로 길어집니다.

GET /products?q=shoes&min=100&max=200&sort=-rating,price&fields=id,name,price&include=reviews&filter.brand.in=nike,adidas&page=2&size=20

URL 길이 제한, 인코딩 지옥, 로그 노출 문제가 모두 겹칩니다. 게다가 중첩 구조나 OR 조건 같은 복잡한 논리를 쿼리 스트링으로 표현하는 표준이 없어 호환성 문제가 끊이지 않습니다.

4.3 GraphQL

GraphQL은 별도의 접근법으로 이 문제를 비껴갑니다. 모든 요청을 단일 엔드포인트로 POST하고, 쿼리 언어로 원하는 데이터를 기술합니다.

POST /graphql HTTP/1.1
Host: api.example.com
Content-Type: application/json

{ "query": "query($cat: String!) { products(category: $cat) { id name price } }", "variables": { "cat": "shoes" } }

GraphQL은 강력하지만, HTTP 수준에서 보면 여전히 모든 것이 POST입니다. HTTP 캐싱과 중간 인프라의 이점을 누리기 어렵고, 별도의 스택과 학습 곡선을 요구합니다. GraphQL이 적합하지 않은 평범한 REST 서비스에는 과한 선택일 수 있습니다.

4.4 우회책 비교

방식본문 사용의미론 정확성캐시 가능성표준성
POST 검색 엔드포인트가능낮음낮음관행적
거대 GET불가보통높음비표준 직렬화
GraphQL가능별도 모델낮음자체 표준
QUERY가능높음높음표준화 진행

5. 실무 적용 — 지금 무엇을 할 수 있는가

5.1 클라이언트에서 QUERY 보내기

대부분의 HTTP 클라이언트는 임의의 메소드 문자열을 허용합니다. curl로는 다음과 같이 보낼 수 있습니다.

curl -X QUERY https://api.example.com/products \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "filter": { "category": "shoes" }, "page": { "size": 20 } }'

JavaScript의 fetch API도 임의 메소드를 지원합니다.

fetch("https://api.example.com/products", {
  method: "QUERY",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ filter: { category: "shoes" } })
})

다만 일부 환경에서는 표준에 없는 메소드를 막을 수 있으므로, 클라이언트 환경별 검증이 필요합니다.

5.2 서버에서 QUERY 받기

서버 프레임워크가 QUERY 메소드를 라우팅으로 인식하는지가 관건입니다. 표준이 확정되기 전이라 많은 프레임워크가 아직 1급 시민으로 지원하지 않습니다. 임시로는 커스텀 메소드 라우팅을 등록하는 방식을 씁니다.

서버 라우팅 의사코드

route(method="QUERY", path="/products", handler=searchProducts)

function searchProducts(request):
    criteria = parseJson(request.body)
    results = repository.search(criteria)
    response.header("Cache-Control", "max-age=60")
    return json(results)

5.3 점진적 도입 전략

당장 전면 도입은 위험합니다. 다음과 같은 단계적 접근을 권합니다.

단계행동
1단계내부 서비스 간 통신에서 먼저 시도하여 인프라 호환성 검증
2단계QUERY와 POST를 동시에 받는 듀얼 라우팅으로 하위 호환성 확보
3단계클라이언트 SDK가 QUERY 미지원 환경에서 POST로 폴백하도록 설계
4단계프록시와 게이트웨이의 QUERY 통과 여부를 모니터링

특히 2단계의 듀얼 라우팅이 현실적인 출발점입니다. 동일한 핸들러에 QUERY와 POST를 모두 매핑해 두면, QUERY를 지원하는 클라이언트는 더 정확한 의미론을 얻고, 그렇지 않은 클라이언트는 기존 POST로 계속 동작합니다.


6. 함정과 비판적 시각

6.1 중간 인프라가 가장 큰 변수

QUERY의 가장 큰 현실적 장벽은 **중간 인프라(intermediaries)**입니다. 인터넷에는 수많은 프록시, 로드밸런서, CDN, 방화벽, API 게이트웨이가 있고, 이들은 자신이 모르는 HTTP 메소드를 만나면 보수적으로 행동하는 경향이 있습니다.

  • 일부 프록시는 알 수 없는 메소드를 그대로 거부(405 또는 501)합니다.
  • 일부는 본문이 있는 안전한 메소드를 예상하지 못해 본문을 누락시킬 수 있습니다.
  • 보안 장비는 비표준 메소드를 잠재적 공격으로 간주해 차단할 수 있습니다.

표준이 RFC로 확정되고 주요 인프라 벤더가 이를 구현하기 전까지, 공용 인터넷을 가로지르는 경로에서 QUERY를 안정적으로 쓰기는 어렵습니다. 그래서 초기 도입은 통제 가능한 내부망에서 시작하는 것이 현실적입니다.

6.2 프레임워크와 도구 지원의 미성숙

현재 시점에서 QUERY를 1급으로 지원하는 웹 프레임워크는 많지 않습니다. 라우팅, 미들웨어, 본문 파싱, OpenAPI 문서화 도구들이 모두 새 메소드를 인지하도록 업데이트되어야 합니다. 이 생태계 전환에는 시간이 걸립니다.

6.3 정말 새 동사가 필요한가라는 회의론

비판적인 목소리도 있습니다. "POST로도 잘 동작하는데 굳이 새 메소드가 필요한가"라는 회의론입니다. 실제로 수많은 검색 API가 POST로 수년간 멀쩡히 운영되어 왔습니다.

이에 대한 반론은 명확합니다. 의미론은 단순한 형식이 아니라 인프라가 올바르게 동작하기 위한 계약입니다. 검색이 안전하고 멱등하다는 사실을 HTTP 수준에서 선언할 수 있으면, 캐시, 재시도, 라우팅, 모니터링이 자동으로 올바르게 작동합니다. POST는 이 정보를 숨기지만 QUERY는 드러냅니다. 장기적으로 이 차이는 작지 않습니다.

6.4 캐싱 명세의 미확정

앞서 본 본문 기반 캐싱은 매력적이지만 아직 완전히 합의되지 않은 영역입니다. 캐시 키 정규화, Content-Location 활용, 중간 캐시의 동작은 드래프트가 진화하면서 바뀔 수 있습니다. 캐싱을 핵심 가치로 보고 QUERY를 도입한다면, 명세 확정 전까지는 보수적으로 접근해야 합니다.

6.5 함정 요약

함정대응 방안
중간 인프라 비호환내부망 우선 도입, 경로상 인프라 사전 검증
프레임워크 미지원커스텀 라우팅 등록, 듀얼 라우팅으로 폴백 확보
캐시 명세 미확정캐싱은 보수적으로, 최신 드래프트 추적
회의론과 조직 저항의미론적 이점을 데이터와 사례로 설득

7. 종합 비교 다이어그램

세 동사가 검색 요청을 처리하는 흐름을 단순화하면 다음과 같습니다.

[클라이언트] --- 검색 요청 ---> [HTTP 메소드] ---> [서버]

GET   : 본문 없음, 모든 조건이 URL에 노출
        안전 / 멱등 / 캐시 가능
        그러나 길이 제한과 표현력 한계

POST  : 본문 있음, 표현력 충분
        그러나 안전 아님 / 멱등 아님 / 캐시 불가
        인프라가 쓰기로 오해

QUERY : 본문 있음, 표현력 충분
        안전 / 멱등 / 캐시 가능 (조건부)
        의미론과 표현력을 동시에 만족

이 그림이 QUERY의 존재 이유를 한 장으로 요약합니다. QUERY는 GET의 의미론적 정확성과 POST의 표현력을 한 동사 안에 담아냅니다.


마치며

HTTP QUERY 메소드는 화려한 신기술은 아닙니다. 오히려 HTTP가 처음부터 비워두었던 빈칸을 뒤늦게 채우는 작업에 가깝습니다. "본문을 가진 안전한 메소드"라는 단순한 아이디어지만, 그 단순함이 REST API 설계자들이 수년간 감수해 온 타협을 해소합니다.

물론 지금 당장 모든 검색 API를 QUERY로 바꿀 수는 없습니다. 표준이 RFC로 확정되어야 하고, 프록시와 프레임워크가 따라와야 하며, 캐싱 명세가 마무리되어야 합니다. 하지만 방향은 분명합니다. GET의 한계와 POST의 어긋남 사이에서 고민하던 오랜 딜레마가, 마침내 의미론적으로 올바른 동사를 갖게 되는 것입니다.

지금 할 수 있는 일은 명확합니다. IETF 드래프트의 진행 상황을 추적하고, 내부 서비스에서 작게 실험하며, 듀얼 라우팅으로 하위 호환성을 확보해 두는 것입니다. 그러면 표준이 확정되는 순간 자연스럽게 전환할 수 있습니다. HTTP의 새로운 동사가 자리 잡는 과정을, 우리는 지금 실시간으로 지켜보고 있습니다.


참고 자료