Go HTTP 요청 본문 이해 및 관리
Wenhao Wang
Dev Intern · Leapcell

소개
Go에서 성능이 뛰어나고 안정적인 웹 서비스를 구축하려면 들어오는 HTTP 요청과의 상호 작용이 자주 필요합니다. 이러한 상호 작용에서 가장 기본적이면서도 자주 오해되는 측면 중 하나는 req.Body를 처리하는 것입니다. 요청 본문은 JSON, XML, 폼 데이터 또는 파일 업로드 등 클라이언트 요청의 페이로드를 전달합니다. 이 스트림을 부적절하게 관리하면 미묘한 버그 및 리소스 누수부터 애플리케이션 충돌에 이르기까지 일련의 문제가 발생할 수 있습니다. 이 문서는 req.Body를 이해하고, 올바른 처리가 왜 중요한지 설명하며, Go 웹 핸들러에서 효율적이고 안전하게 처리하는 방법에 대한 실용적인 지침을 제공할 것입니다.
Go HTTP 요청 본문 설명
"방법"으로 들어가기 전에 req.Body가 실제로 무엇인지, 그리고 그 특성이 왜 특별한 처리를 요구하는지 명확히 해 봅시다.
Go의 net/http 패키지에서 http.Request.Body는 io.ReadCloser 유형입니다. 이 인터페이스를 이해하는 것이 중요합니다.
io.Reader: 이는 일반적으로 순차적으로 데이터를 읽을 수 있다는 것을 의미합니다. 데이터를 읽으면 일반적으로 소비되며 특별한 조치 없이는 동일한io.Reader인스턴스에서 다시 읽을 수 없습니다.io.Closer: 이는 완료 시Close()메서드를 호출해야 한다는 것을 의미합니다. 이는 네트워크 연결 또는 파일 설명자와 같은 기본 리소스를 해제하는 데 필수적입니다. 본문을 닫지 않으면 리소스 누수가 발생하고 HTTP 클라이언트가 이후 요청에 클라이언트 연결을 재사용하지 못하게 할 수 있습니다 (연결 유지 관리가 활성화된 경우).
io.ReadCloser의 주요 의미:
- 단일 읽기: 요청 본문은 스트림입니다. 
req.Body에 사용되는 것을 포함하여 대부분의io.Reader구현은 한 번만 읽을 수 있습니다. 두 번째 읽기를 시도하면 빈 스트림이나 오류가 발생할 가능성이 높습니다. - 리소스 관리: 
Close()메서드는 선택 사항이 아닙니다. HTTP 서버가 본문 처리를 완료했음을 기본 HTTP 서버에 알리므로 서버가 리소스를 정리하고, 중요한 것은 클라이언트 연결을 재사용할 수 있도록 합니다. 
올바른 처리가 필수적인 이유
req.Body의 특성을 무시하면 여러 가지 문제가 발생할 수 있습니다.
- 리소스 누수: 가장 일반적인 문제입니다. 
Close()를 호출하지 않으면 네트워크 연결이 불필요하게 오래 열려 있어 사용 가능한 파일 설명자가 고갈되고 부하가 걸릴 때 "열린 파일이 너무 많음" 오류가 발생할 수 있습니다. - 연결 재사용 실패: 연결 유지 관리 연결의 경우 본문을 소비하고 닫지 않으면 클라이언트가 동일한 연결을 통해 후속 요청을 보내지 못할 수 있으며, 각 요청에 대해 새 TCP 연결을 강제 실행하여 성능에 영향을 미칩니다.
 - 예상치 못한 동작: 코드가 여러 부분에서 올바른 버퍼링 없이 동일한 
req.Body를 읽으려고 하면 첫 번째 리더만 성공하여 혼란스러운 버그로 이어집니다. - 성능 병목 현상: 루프에서 바이트 단위로 읽는 것과 같이 비효율적인 읽기는 버퍼링된 읽기 또는 
io.Util.ReadAll을 사용하는 것보다 훨씬 느릴 수 있습니다. 
req.Body를 올바르게 처리하는 방법
req.Body에 대한 황금률은 다음과 같습니다. 항상 읽고, 항상 닫으십시오.
일반적인 시나리오와 모범 사례를 살펴보겠습니다.
1. 본문 폐기
핸들러가 실제 요청 본문을 필요로 하지 않는 경우(예: 예상치 못한 본문이 있는 GET 요청 또는 본문에 관련 정보가 없는 POST 요청), 여전히 본문을 비우고 닫아야 합니다.
package main import ( "fmt" "io" "net/http" ) func discardBodyHandler(w http.ResponseWriter, req *http.Request) { //defer를 사용하여 오류 발생 시에도 본문이 닫히도록 보장합니다. defer req.Body.Close() // 본문을 비우고 모든 콘텐츠를 소비합니다. // 연결 재사용 및 리소스 정리에 중요합니다. io.Copy(io.Discard, req.Body) w.WriteHeader(http.StatusOK) fmt.Fprintln(w, "Body discarded successfully!") } func main() { http.HandleFunc("/discard", discardBodyHandler) fmt.Println("Server listening on :8080") http.ListenAndServe(":8080", nil) }
설명:
defer req.Body.Close(): 이것이 가장 중요한 부분입니다.defer는 함수가 반환되기 직전에Close()가 성공 또는 오류와 관계없이 호출되도록 합니다.io.Copy(io.Discard, req.Body):io.Discard는 작성된 모든 데이터를 버리는 미리 할당된io.Writer입니다. 이는req.Body의 전체 내용을 효과적으로 읽고 버려 전체적으로 소비되도록 합니다.
2. JSON 데이터 읽기
이것은 매우 일반적인 사용 사례입니다.
package main import ( "encoding/json" "fmt" "io" "net/http" ) type User struct { Name string `json:"name"` Email string `json:"email"` } func createUserHandler(w http.ResponseWriter, req *http.Request) { defer req.Body.Close() // 항상 defer Close()를 사용합니다. // 남용을 방지하기 위해 요청 본문의 크기를 제한합니다. // 예를 들어, 1MB 제한 req.Body = http.MaxBytesReader(w, req.Body, 1048576) var user User // json.NewDecoder는 스트림에서 직접 읽습니다. err := json.NewDecoder(req.Body).Decode(&user) if err != nil { http.Error(w, fmt.Sprintf("Error decoding JSON: %v", err), http.StatusBadRequest) return } fmt.Printf("Received user: %+v\n", user) w.WriteHeader(http.StatusCreated) fmt.Fprintf(w, "User %s created successfully!", user.Name) } func main() { http.HandleFunc("/users", createUserHandler) fmt.Println("Server listening on :8080") http.ListenAndServe(":8080", nil) }
설명:
defer req.Body.Close(): 여전히 필수적입니다.http.MaxBytesReader(w, req.Body, 1048576): 이것은 중요한 보안 조치입니다.req.Body를 래핑하고 여기서 읽을 수 있는 바이트 수를 제한합니다. 클라이언트가 1MB보다 더 많이 보내면 디코더가 오류를 발생시켜 큰 페이로드가 서버 리소스를 소비하거나 서비스 거부 공격에 사용되는 것을 방지합니다.json.NewDecoder(req.Body).Decode(&user):json.NewDecoder는 스트림에서 직접 읽기 때문에 효율적입니다. 먼저 전체 본문을 메모리에 로드하지 않으므로 큰 페이로드에 더 좋습니다. 디코딩하는 동안 본문을 비우는 것도 처리합니다.
3. 본문 읽기 및 다시 읽기 (버퍼링)
때로는 원시 본문을 먼저 검사해야 할 수도 있습니다(예: 로깅용). 그런 다음 다른 함수에 전달하거나 디코딩합니다. req.Body는 한 번만 읽을 수 있으므로 버퍼링해야 합니다.
package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) type Product struct { ID string `json:"id"` Price float64 `json:"price"` } func logAndProcessProductHandler(w http.ResponseWriter, req *http.Request) { defer req.Body.Close() // 원본 본문을 닫습니다. // 전체 본문을 버퍼로 읽습니다. bodyBytes, err := io.ReadAll(req.Body) if err != nil { http.Error(w, "Error reading request body", http.StatusInternalServerError) return } // 원시 본문을 기록합니다. fmt.Printf("Raw request body: %s\n", string(bodyBytes)) // 후속 처리를 위해 버퍼링된 바이트에서 새 리더를 만듭니다. bodyReader := bytes.NewReader(bodyBytes) var product Product err = json.NewDecoder(bodyReader).Decode(&product) // 새 리더에서 디코딩합니다. if err != nil { http.Error(w, fmt.Sprintf("Error decoding JSON: %v", err), http.StatusBadRequest) return } fmt.Printf("Processed product: %+v\n", product) w.WriteHeader(http.StatusCreated) fmt.Fprintf(w, "Product %s processed successfully!", product.ID) } func main() { http.HandleFunc("/products", logAndProcessProductHandler) fmt.Println("Server listening on :8080") http.ListenAndServe(":8080", nil) }
설명:
defer req.Body.Close(): 원본 리더에게 필수적입니다.io.ReadAll(req.Body):req.Body의 전체 내용을 바이트 슬라이스로 읽습니다. 매우 큰 페이로드를 다룰 때는 주의하십시오. 이 작업은 전체 본문을 메모리로 로드합니다. 매우 큰 파일의 경우 스트리밍 또는 청크 단위 처리를 고려하십시오.bytes.NewReader(bodyBytes):bodyBytes슬라이스에서 읽는 새io.Reader를 생성합니다. 이 새 리더는 필요한 경우 여러 번 읽거나 다른 함수에 전달할 수 있습니다.
모범 사례 요약
- 항상 
defer를 사용하여req.Body.Close()를 즉시 호출하십시오: 이것이 가장 중요한 규칙입니다. http.MaxBytesReader고려: 리소스 고갈 및 DoS 공격으로부터 보호하기 위해 들어오는 본문의 크기를 제한하십시오.- 구조화된 데이터의 경우 
json.NewDecoder또는xml.NewDecoder사용: 스트림에서 직접 읽으며 효율적이며 일반적으로 본문을 비우는 것을 처리합니다. - 본문이 필요하지 않으면 
io.Copy(io.Discard, req.Body)사용: 적절한 정리 및 연결 재사용을 보장합니다. - 필요한 경우에만 버퍼링: 원시 본문을 다시 읽거나 기록해야 하는 경우 
io.ReadAll및bytes.NewReader가 도구이지만 큰 본문의 경우 메모리 사용량을 고려하십시오. - 오류 처리: 본문을 읽거나 디코딩한 후에는 항상 오류를 확인하십시오.
 
결론
Go의 net/http 패키지에서 req.Body는 강력한 스트림이지만 io.ReadCloser라는 특성 때문에 세심한 주의가 필요합니다. defer req.Body.Close()를 일관되게 적용하고, 언제 비우고, 직접 디코딩하고, 버퍼링해야 하는지를 이해함으로써 Go 웹 애플리케이션이 견고하고 리소스 누수가 없으며 성능과 보안을 유지하도록 보장합니다. 요청 본문을 적절하게 처리하는 것은 실제 요구 사항을 충족하는 고품질 Go 웹 서비스를 작성하는 기본적인 측면입니다.