Go 1.27의 HTTP/1 연결 재사용, 본문 정리 코드는 어디까지 줄일 수 있을까
Go 1.27은 닫힌 HTTP/1 응답의 남은 본문을 제한적으로 읽어 연결 재사용을 시도한다. Go 1.27.2 구현의 크기·시간 조건과 추가 수신 비용을 살펴보고, 기존 수동 정리 코드를 줄일 판단 기준을 정리한다.
상태 코드만 필요한 HTTP 요청에서도 서버는 본문을 보낼 수 있다. 응답을 받고 Body.Close()까지 호출했는데, 다음 요청마다 연결을 새로 만든다면 코드가 놓친 것은 본문의 끝이다. Go 1.27의 HTTP/1 개선은 이 틈을 표준 라이브러리가 메우는 변화다. 다만 기존의 본문 읽기 코드를 지우려면 어떤 응답까지 연결을 살릴 수 있는지 먼저 알아야 한다.
Go 팀의 설명 글은 2026년 10월 14일 공개됐고, GeekNews에는 10월 15일 오전 6시 42분, 한국 시각으로 등록됐다. 8월에 나온 Go 1.27의 동작을 설명한 글이며, 10월 15일 공식 다운로드에서 확인한 최신 버전은 1.27.2다.
닫았다는 사실과 끝까지 읽었다는 사실
HTTP/1에서는 한 연결에 실린 응답을 순서대로 읽는다. http.Get은 상태 줄과 헤더를 읽은 뒤 반환하고, 본문은 호출자가 resp.Body에서 읽는다. 다음 응답의 시작을 찾으려면 현재 본문의 끝까지 도달해야 한다. EOF는 더 읽을 데이터가 없다는 신호다. 공식 설명
Go 1.26까지는 본문을 남겨 둔 채 닫으면 그 HTTP/1 연결을 포기했다. 상태 코드만 확인하는 함수나 JSON 값 하나를 읽고 반환하는 코드도 이 조건에 걸릴 수 있다. JSON 해석을 마쳤다는 사실은 HTTP 본문에서 EOF를 확인했다는 보장이 아니다. 버퍼에 더 읽힌 데이터가 있는지와 실제 본문 끝까지 소비했는지도 구분해야 한다.
연결을 버리면 이후 요청은 TCP 연결과 TLS 협상을 다시 거칠 수 있다. 실제 비용은 거리, TLS 버전, 서버 처리 시간에 따라 달라진다. HTTP/2는 여러 요청을 독립된 스트림으로 한 연결에 실으므로, 한 본문이 남았다고 다른 요청의 응답 경계를 찾지 못하는 같은 문제가 생기지는 않는다. 그렇다고 HTTP/2에서 본문을 닫을 책임까지 사라지는 것은 아니다. 프로토콜별 차이
재사용을 시도하되 읽는 비용을 제한한다
예전의 대응은 남은 본문을 읽어 버리는 drain이었다. 작은 응답이라면 연결을 살릴 수 있지만, 큰 파일이나 천천히 끝나는 응답을 전부 받으면 새 연결보다 비쌀 수 있다. 크기와 시간 제한, 취소와 백그라운드 처리까지 직접 구현해야 했던 이유다.
Go 1.27의 net/http.Transport는 본문이 일찍 닫히면 제한된 범위에서 비동기로 읽어 버린다. 공식 글은 256KiB와 50ms를 기준으로 설명하며, Connection: close처럼 재사용할 수 없는 응답은 이 작업을 건너뛴다고 밝힌다. 호출자는 필요한 처리를 마친 뒤 본문을 닫고, 연결을 살리는 세부 작업은 표준 구현에 맡길 수 있다. 권장 사용법
내가 주목한 부분은 이 한도가 남은 본문 크기만으로 결정되지 않는다는 점이다. Go 1.27.2 구현은 응답의 ContentLength가 256KiB보다 크면 자동 읽기를 시도하지 않는다. 이 값은 남은 양이 아니라 응답 전체 길이를 나타낸다. 따라서 큰 응답의 끝부분만 남겼다고 같은 혜택을 기대할 수 없다. 길이를 모르는 응답에는 실제 읽기 제한이 작동한다. 소스에서 확인한 조건이며 실행으로 재현한 결과는 아니다.
또한 Close()가 반환될 때 재사용 준비가 끝났다는 보장도 없다. 뒤따르는 요청이 먼저 시작되면 새 연결을 만들고, 정리된 연결은 더 나중 요청에서 쓰일 수 있다. 256KiB·50ms라는 값도 향후 버전에서 바뀔 수 있는 구현 기준으로 읽어야 한다. 연결 재사용은 가능한 경우의 최적화이며, 애플리케이션이 응답을 정확히 처리했다는 증명은 아니다.
다섯 배라는 숫자를 우리 요청으로 옮기기 전에
공식 글의 실험은 HTTP/1을 강제하고 index.golang.org의 상태 코드를 100회 확인했다. 저자의 머신에서 Go 1.26은 약 710ms, 1.27은 약 150ms였다. 본문을 읽지 않는 패턴에서 연결 생성 비용이 줄어든 결과다. 이 글에서는 해당 프로그램을 실행하거나 성능을 재측정하지 않았다. 실험 조건
이미 본문을 끝까지 읽는 클라이언트, HTTP/2 위주인 경로, 서버 계산 시간이 긴 요청에는 같은 배수의 개선을 기대하기 어렵다. 반대로 같은 호스트의 작은 응답을 자주 받고 상태나 일부 필드만 사용하는 클라이언트라면 살펴볼 가치가 있다.
비용이 늘 수도 있다. 호출자는 관심이 없는 데이터를 추가로 받게 되고, 읽기를 맡는 백그라운드 작업과 유휴 연결이 생긴다. 릴리스 노트는 서로 다른 클라이언트를 요청마다 사용하거나 유휴 연결 제한을 잘못 설정한 프로그램에서 성능이 나빠질 수 있다고 설명한다. 유휴 연결은 다음 요청을 기다리는 연결이다. 기존 Transport의 공유 방식과 MaxIdleConns 같은 풀 제한도 함께 검토해야 한다. 새 기능을 확인하겠다고 모든 요청의 연결 유지부터 끄면 평가할 조건 자체가 사라진다.
본문 정리 함수를 지우는 순서
나는 먼저 실제 배포 바이너리를 빌드한 Go 버전과 사용 중인 전송 구현을 확인하겠다. 개발 머신에 Go 1.27을 설치해도 기존 바이너리의 표준 라이브러리는 바뀌지 않는다. 사용자 정의 전송 구현이나 아직 1.26을 지원하는 공용 라이브러리라면 기존 처리를 같은 조건으로 제거할 수 없다.
다음에는 수동 drain을 둔 이유를 읽는다. 연결 재사용만을 위한 것인지, 본문 오류를 확인하거나 응답 데이터를 검증하는 역할까지 있는지 구분한다. 자동 읽기는 상태 코드의 의미, JSON 뒤의 불필요한 데이터, 응답 크기 제한을 대신 판단하지 않는다. 필요한 데이터를 다루는 코드와 연결을 정리하는 코드를 함께 지워서는 안 된다.
비교할 응답은 다음처럼 나누겠다. 아래는 도입 전 검증 제안이다.
| 응답 조건 | 확인할 변화 |
|---|---|
| 작은 HTTP/1 본문을 일부만 읽는다 | 새 연결 수와 재사용률, 요청 지연 |
| 전체 길이가 큰 본문이나 느린 스트림이다 | 추가 수신량, 읽기 종료와 취소, 연결 폐기 |
HTTP/2 또는 Connection: close다 | 불필요한 수동 읽기가 남아 있는지, 오류 처리 |
연결 재사용은 공식 httptrace의 GotConnInfo.Reused로 관찰할 수 있다. 이는 해당 연결이 이전 HTTP 요청에 쓰였는지를 나타낸다. 요청 지연과 함께 새 연결 수, 수신 바이트, 오류율도 비교한다. 연속 요청과 동시 요청을 나누고, 요청 기한과 취소가 있는 경우도 포함해야 정리 비용이 어디에 남는지 보인다.
작은 HTTP/1 응답을 반복해서 받는 경로에서 재사용이 늘고 추가 비용이 감당할 만하다면, 연결 유지를 위해 만든 수동 drain을 줄일 근거가 된다. 큰 응답이나 오래된 버전 지원이 필요하다면 범위를 달리 정한다. 호출자가 본문을 닫을 책임을 지키면서, 어느 연결을 어떤 비용으로 살릴지는 표준 구현에 맡길 수 있는 범위가 넓어졌다. 그 경계를 확인한 뒤 정리 코드를 줄이겠다.
참고 자료
이 이야기가 도움이 되었나요?