Go 1.27의 HTTP/1 연결 재사용 개선
요약
Go 언어의 HTTP 클라이언트 연결 재사용 및 본문 처리 방식에 대한 논의입니다. 특히, 응답 본문을 완전히 소비하지 않았을 때 발생하는 연결 종료(Close) 동작과 관련된 까다로운 경계 조건들을 다루고 있습니다. 개발자들은 이 과정에서 필요한 명확한 문서화와 더 나은 API 설계 방안을 모색하고 있습니다.
핵심 포인트
- Go의 HTTP 클라이언트는 본문을 완전히 소비하지 않으면 비동기적으로 데이터를 읽으려 시도합니다.
- 응답 연결을 즉시 강제 종료하려면 하위 네트워킹 계층에 대한 이해가 필요할 수 있습니다.
- JSON 디코딩 후 남은 공백 처리를 위해 Decoder.More()를 확인하는 것이 권장됩니다.
- encoding/json/v2의 UnmarshalRead는 단일 JSON 문서 외의 경우에도 유용한 대안입니다.
HTTP/3의 표준 라이브러리 지원은 언제 추가될지 궁금함. Go가 빠르게 성장한 핵심 요인 중 하나는 별도 설치 없이도 단순하면서 강력한 HTTP 기능을 쓸 수 있다는 점임.
외부 패키지 없이 완전한 웹 앱을 만들 수 있다는 매력이 있으며, 이 정도 느낌을 받은 언어는 PHP 정도임. Racket도 웹 개발을 위해 따로 많은 설정이 필요하지 않은 것 같아 여기에 들어갈 수 있음.
Go 팀이 자동 연결 재사용을 구현하기로 한 계기가 끔찍하지만 올바른 세 번째 drainAndCloseBody 구현을 실제로 마주쳤기 때문 아닐까 싶어 재미있음. drainAndCloseBody 제안도, 타이머를 넣은 drainAndCloseBodyWithTimer도, 비동기 처리까지 더한 drainAsyncAndCloseBodyWithTimer도 괜찮다더니, Google 누군가가 세 번째 구현을 HTTP/2 연결에 쓰자 비로소 선을 그은 셈일까?
흔하지만 까다로운 경계 조건에서 Go의 단순함을 지키기 어렵다는 데 공감함. 세 번째 구현은 타당하지만 정말 보기 흉함.
내가 놓친 게 아니라면 이 동작은 net/http 문서 어디에도 없는 것 아닌가? Close라는 이름에서 예상하기 어려운 구현이니 문서에 명시해야 함.
Go를 HTTP 클라이언트로 쓰려는 사람들이 자주 묻는 내용이라, 다소 찜찜해도 변경 취지는 이해함. 다만 재사용할 생각이 없는 소켓에서 불필요하게 데이터를 읽지 않고 연결을 즉시 강제 종료하려면 어떻게 해야 하는지 모르겠음. 응답 객체에 가짜 Connection: close 헤더를 넣어야 할까, 아니면 본문의 구체 타입에서 하위 소켓을 찾아 이 코드를 우회해야 할까? 이를 위한 방법도 있어야 함.
HTTP 라이브러리의 추상화는 원래 어려움. 실제 HTTP 사용에는 최소한 DNS와 TLS, 흔히 L4 네트워킹까지 이해해야 함.
애플리케이션 개발자에게 맞추면 사용하는 HTTP 버전조차 모르는데 읽지 않은 HTTP/1 본문을 비워야 하는 식으로 추상화가 새어 나옴. 반대로 충분히 저수준으로 만들면 애플리케이션 개발자가 쓰기 어려워짐. Rust의 hyper와 reqwest가 이런 계층 분리를 시도한 예임.
Client.Do 문서에 명시되어 있음. https://pkg.go.dev/net/http#Client.Do.
본문을 닫으면 Transport가 보수적으로 정한 한도 내에서 응답 본문을 EOF까지 비동기로 읽으려 시도한다고 적혀 있음. 다른 곳에도 비슷한 문구가 있으니 “asynchronously”를 검색하면 됨.
Close는 HTTP 요청 관점의 동작이므로 여기에 직접 문서화하지 않아도 타당해 보임. HTTP/2에서는 요청의 수명주기가 하위 소켓과 분리되어 있어, 애초에 기대한 대로 연결을 닫는 동작이 아님.
정말 즉시 종료하려면 하위 계층을 알아야 한다는 불편함은 있어도 연결을 직접 가져와 강제로 닫는 편이 논리적임. 내가 잘못 이해한 게 아니라면 본문이 없고 연결을 유지하는 경우에도 Close가 실제 연결을 닫지는 않는 것 아닌가?
CloseNow() error 메서드가 있으면 합리적일 것 같음.
JSON Decoder 로 본문을 읽어도 응답에 줄바꿈이 있으면 연결이 재사용되지 않을 수 있음. Decoder는 일정 크기 단위로 읽는데, 정확히 기억나지는 않지만 512바이트였던 것 같음. JSON 구조가 그 크기나 배수에 딱 맞으면 줄바꿈이 남게 됨.
그래서 디코딩 후에는 항상 Decoder.More 를 확인해 남은 공백을 읽어 없앰.
Decoder는 편리하지만, 뒤에 아무것도 붙지 않은 단일 JSON 문서라는 조건을 벗어날 때의 경계 조건 때문에 이런 용도에는 권장되지 않음.
표준 라이브러리 관리자들도 더 편하고 나은 방법이 없다는 점을 인지했고, encoding/json/v2의 UnmarshalRead 를 마련함. https://pkg.go.dev/encoding/json/v2#UnmarshalRead.
본문을 전혀 읽지 않은 경우만 최적화하는 게 아니라, 읽지 않은 데이터 최대 256K를 읽어 비우는 방식임. https://cs.opensource.google/go/go/+/refs/tags/go1.27.2:src/...
다만 코드를 보면 transfer-encoding:chunked 스트리밍 응답은 처리하지 않음. 상대편이 json.NewEncoder(responseWriter).Encode(&someStruct)를 쓰는 경우에는 이 최적화가 효과를 내지 못함.
전혀 몰랐던 경계 조건임. 두 패키지를 함께 쓸 때 발생하는 동작이라 어디서 문서를 찾아야 할지도 막막함.
나도 r60부터 프로젝트와 회사를 옮길 때마다 복사해 쓰던 비슷한 보조 함수를 드디어 제거할 수 있게 됨.
예전에는 응답 본문을 닫으면 본문 다운로드를 완전히 건너뛸 수 있었는데, 내부 동작이 달라지는 건 아쉬움. 그래도 다운로드 시간을 50ms로 제한하니 이 절충안은 마음에 듦. 취소한 응답 본문들을 네트워크가 감당하지 못하더라도 오래 붙들리지 않고 약간의 트래픽만 더 쓰게 됨.
마지막에 Connection: close이면 본문을 읽어 비우지 않는다고 하는데, 역방향 프록시에서도 잘 동작할까?
서버가 본문을 소비하지 않을 때 nginx에서 입출력 오류가 나고 클라이언트에는 5xx로 전달되는 일을 겪었음. Firefox에서도 XHR 상태 코드가 0이 되는 현상을 본 것 같음. 반면 Chrome과 curl에서는 본문을 읽지 않고 일찍 닫아도 예상대로 동작하는 듯함.
GODEBUG=http2client=0으로 HTTP/1을 사용해 index.golang.org에 100회 요청하면 Go 1.26은 약 710ms, Go 1.27은 약 150ms가 걸린다고 함.
이 기능을 쓰려면 HTTP/2를 꺼야 한다는 뜻인가, 아니면 단지 HTTP/2를 사용하지 않는다는 점을 확인하기 위한 설정인가?
글에서 설명하듯 HTTP/2에서는 이 처리가 필요하지 않음. 프로토콜 설계상 본문을 끝까지 읽어 비우지 않아도 연결 공유의 이점을 얻을 수 있음.
HTTP/2는 하나의 TCP 연결에서 다중화와 우선순위 지정이 가능해 동작 방식이 다름. 다만 기억이 맞다면 웹의 60~70%는 여전히 HTTP/1을 사용함.
2026년에도 HTTP/1을 쓸 이유가 있을까?
주로 연동하는 서버나 클라이언트가 HTTP/2를 지원하지 않기 때문임. CDN에서 커널 TLS와 sendfile 을 쓰는 경우처럼 다른 이유도 있음. HTTP/1.1 상태 줄과 content-length 헤더를 소켓에 쓴 뒤 sendfile(sock, f)로 파일을 보내는 방식은 HTTP/2나 HTTP/3에서 그대로 쓸 수 없음. WebSocket도 실질적으로는 HTTP/1만 사용함. 다른 버전을 위한 명세는 있지만 브라우저에서 비활성화되어 있음. HTTP를 하나의 큰 양방향 통신 경로를 여는 용도로만 쓴다면, HTTP/2의 추가 프레임 처리와 흐름 제어는 성능 부담일 뿐임.
SSL을 쓰지 않으면 HTTP/2나 HTTP/3를 쓸 수 없음. 외부 공개 서비스에서는 적절하지 않지만, 로컬 네트워크에서는 괜찮거나 피할 수 없는 경우도 있음.
믿기 어렵겠지만 CloudFront는 원본 서버 연결에 HTTP/1만 지원함. 다른 CDN 중에도 그런 곳이 있음.
직접 제어할 수 없는 타사 백엔드에 요청하는 경우도 있지 않을까?
AJAX 중심 웹 앱을 만드는 개발자에게는 전환 이유가 명확하지만, 백엔드 개발자는 설득이 필요해 보임.
AI 자동 생성 콘텐츠
본 콘텐츠는 RSS: GeekNews (한국어)의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기