API 스펙을 전달받았는데, GET 요청에 필터 조건을 request body로 보내달라는 내용이었습니다.
GET /api/products
Content-Type: application/json
{
"category": "electronics",
"priceRange": { "min": 10000, "max": 50000 },
"tags": ["sale", "new"]
}
"Query string이 복잡해지니까 body로 받는 게 깔끔하지 않나요?"라는 이유였습니다. 하지만 GET 요청에 body를 담는 건 표준적인 방식이 아닙니다. Axios는 GET body를 지원하지 않고, 브라우저의 Fetch API도 body를 무시합니다.
이 문서는 왜 그런지 조사한 내용을 정리한 것입니다. GET/DELETE 요청에 body를 넣는 것이 왜 문제인지, RFC 스펙은 무엇을 말하는지, 그리고 어떤 대안이 있는지 다룹니다.
RFC 스펙이 말하는 것
RFC 7231 (HTTP/1.1, 2014)
GET 요청:
"A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request."
DELETE 요청:
"A payload within a DELETE request message has no defined semantics; sending a payload body on a DELETE request might cause some existing implementations to reject the request."
RFC 9110 (HTTP Semantics, 2022 - 최신)
최신 HTTP 스펙인 RFC 9110은 더 강한 경고를 추가했습니다:
"A client SHOULD NOT generate content in a GET request unless it is made directly to an origin server that has previously indicated, in or out of band, that such a request has a purpose and will be adequately supported."
핵심 포인트
주의
"no defined semantics"의 의미 - 금지된 것은 아님 (NOT forbidden) - 하지만 의미가 정의되지 않음 (undefined behavior) - 서버가 무시해도 됨 - 서버가 거부해도 됨 - 어떤 일이 일어날지 보장 없음
참고로 유일하게 body가 명시적으로 금지된 메서드는 TRACE입니다:
"A client MUST NOT send a message body in a TRACE request."
실제로 발생하는 문제들
// ❌ 많은 환경에서 body가 전송되지 않음
fetch('/api/users', {
method: 'GET',
body: JSON.stringify({ filter: 'active' }) // 무시될 수 있음
});클라이언트별 동작
Fetch API (브라우저)
// GET + body
fetch("/api/data", {
method: "GET",
body: JSON.stringify({ query: "test" }),
});
// ⚠️ Chrome, Firefox, Safari 모두 body 무시됨 (전송되지 않음)
// DELETE + body
fetch("/api/items/1", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reason: "duplicate" }),
});
// ✅ 대부분 브라우저에서 전송됨 (하지만 서버에서 문제 가능)Axios
// ❌ GET + body - 지원하지 않음
axios.get("/api/data", {
data: { query: "test" }, // 무시됨
});
// ⚠️ DELETE + body - 특수 문법 필요
axios.delete("/api/items/1", { reason: "duplicate" }); // ❌ 작동 안함
// 올바른 방식 (작동할 수 있음)
axios.delete("/api/items/1", {
data: { reason: "duplicate" }, // data 속성 사용
});
// 또는 axios() 직접 호출
axios({
method: "delete",
url: "/api/items/1",
data: { reason: "duplicate" },
});정보
Axios 공식 문서:
"
datais the data to be sent as the request body. Only applicable for request methods 'PUT', 'POST', 'DELETE', and 'PATCH'"
GET은 명시적으로 제외됩니다.
클라이언트 호환성 요약
| 클라이언트 | GET + body | DELETE + body |
|---|---|---|
| Fetch (Chrome) | ❌ 무시됨 | ⚠️ 전송됨 |
| Fetch (Firefox) | ❌ 무시됨 | ⚠️ 전송됨 |
| Fetch (Safari) | ❌ 무시됨 | ⚠️ 전송됨 |
| Axios | ❌ 지원 안함 | ⚠️ data 속성 필요 |
| XMLHttpRequest | ⚠️ 브라우저마다 다름 | ⚠️ 전송됨 |
| cURL | ⚠️ 전송됨 | ⚠️ 전송됨 |
| Postman | ⚠️ 전송됨 | ⚠️ 전송됨 |
서버/프레임워크별 동작
Node.js (Express)
const express = require("express");
const app = express();
app.use(express.json()); // body-parser
app.get("/api/data", (req, res) => {
console.log(req.body); // {} 또는 undefined
// GET 요청의 body는 파싱되지 않을 수 있음
});
app.delete("/api/items/:id", (req, res) => {
console.log(req.body); // 대부분 작동하지만 보장 없음
});Spring Boot (Java)
// GET + body - 기본적으로 지원하지 않음
@GetMapping("/api/data")
public Response getData(@RequestBody FilterDto filter) {
// ⚠️ 작동할 수 있지만 권장하지 않음
}
// DELETE + body
@DeleteMapping("/api/items/{id}")
public Response deleteItem(
@PathVariable Long id,
@RequestBody DeleteOptionsDto options // ⚠️ 권장하지 않음
) {
// ...
}서버 호환성 요약
| 환경 | GET body 파싱 | DELETE body 파싱 |
|---|---|---|
| Express.js | ⚠️ 기본 미지원 | ✅ 대부분 작동 |
| Spring Boot | ⚠️ 설정 필요 | ✅ 대부분 작동 |
| Django | ❌ 기본 미지원 | ⚠️ DRF 설정 필요 |
| ASP.NET Core | ⚠️ 설정 필요 | ✅ 대부분 작동 |
| nginx (프록시) | ⚠️ 제거될 수 있음 | ⚠️ 제거될 수 있음 |
| AWS API Gateway | ❌ 거부 | ⚠️ 설정에 따라 다름 |
올바른 대안
GET 요청에 필터/검색 조건 전달
// ❌ Body 사용 (안티패턴)
fetch('/api/users', {
method: 'GET',
body: JSON.stringify({
filter: 'active',
sort: 'name',
page: 1
})
});DELETE 요청에 추가 정보 전달
// ✅ Query Parameters
fetch('/api/items/123?reason=duplicate&force=true', {
method: 'DELETE'
});대량 삭제 (Bulk Delete)
// ❌ DELETE + body로 ID 목록 전달 (안티패턴)
fetch("/api/items", {
method: "DELETE",
body: JSON.stringify({ ids: [1, 2, 3, 4, 5] }),
});
// ✅ 방법 1: Query Parameters (소량)
fetch("/api/items?ids=1,2,3,4,5", { method: "DELETE" });
// ✅ 방법 2: POST 사용 (대량)
fetch("/api/items/bulk-delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ids: [1, 2, 3, 4, 5] }),
});대안 비교
| 방법 | 장점 | 단점 | 적합한 경우 |
|---|---|---|---|
| Query Params | 캐시 가능, 호환성 최고 | URL 길이 제한 | 간단한 필터, 소량 ID |
| Custom Headers | 깔끔한 URL | 비표준, 디버깅 어려움 | 메타데이터 전달 |
| POST 변경 | Body 사용 가능, 복잡한 데이터 | REST 의미론 훼손 | 복잡한 작업, 대량 처리 |
| 별도 리소스 | REST 원칙 준수, 확장성 | 복잡한 API 설계 | 비동기 작업, 감사 로그 |
정리
GET/DELETE + Body = 지뢰밭
- RFC 스펙: "no defined semantics" = 무슨 일이 일어날지 모름 - 브라우저: GET body는 대부분 무시됨 - Axios: GET body 지원 안함, DELETE는 특수 문법 필요 - 프록시/CDN: body를 제거하거나 캐시 문제 발생 가능 - 서버: 파싱 안 할 수도, 거부할 수도 있음 - API 도구: 경고/에러 발생 (Swagger, OpenAPI)
권장 방법
| 상황 | 권장 방법 |
|---|---|
| GET + 필터/검색 | Query Parameters |
| DELETE + 단순 옵션 | Query Parameters 또는 Headers |
| DELETE + 복잡한 데이터 | POST로 변경 |
| 대량 삭제 | POST /bulk-delete 엔드포인트 |