HTTP 요청 하나를 보내려면 그 아래에서 TCP 연결이 열려 있어야 합니다. 연결을 어떻게 열고, 재사용하고, 닫는가는 눈에 잘 안 보이지만 지연 시간과 처리량을 좌우하는 핵심입니다. 특히 매 요청마다 새 연결을 여는 것은 값비싸므로, 현대 HTTP는 연결을 최대한 재사용하도록 진화해 왔습니다.
TCP 연결 수명주기
TCP 연결은 3-way 핸드셰이크(SYN → SYN-ACK → ACK)로 시작해, 데이터를 주고받은 뒤 4-way 종료(FIN/ACK 교환)로 닫힙니다. HTTPS라면 그 위에 TLS 핸드셰이크가 더해집니다. 즉 첫 요청 한 번을 보내기까지 TCP 왕복 + TLS 왕복이 겹쳐, 지리적으로 먼 서버라면 실제 데이터가 오기 전에 이미 수백 밀리초가 소요될 수 있습니다. 이 고정 비용을 여러 요청이 나눠 쓰게 하는 것이 연결 재사용의 동기입니다.
keep-alive와 지속 연결
HTTP/1.0은 기본적으로 요청 하나를 처리하면 연결을 닫았습니다. HTTP/1.1은 반대로 지속 연결(persistent connection)을 기본값으로 삼아, 한 번 연 연결로 여러 요청을 연달아 처리합니다. 연결을 닫고 싶을 때만 명시적으로 Connection: close를 보냅니다. 이 덕분에 핸드셰이크 비용을 한 번만 치르고 이어지는 요청들은 곧바로 재사용된 연결로 나갑니다.
GET /page1 HTTP/1.1
Host: example.com
Connection: keep-alive
HTTP/1.1 200 OK
Keep-Alive: timeout=5, max=100
Connection: keep-alive
커넥션 풀링
브라우저와 HTTP 클라이언트 라이브러리는 열어 둔 연결을 풀(pool)에 보관해 두고, 같은 출처로 가는 다음 요청에 재사용합니다. HTTP/1.1에서는 연결 하나가 한 번에 하나의 요청만 처리하므로, 병렬성을 위해 클라이언트가 호스트당 여러 개(관례적으로 약 6개)의 연결을 동시에 엽니다. HTTP/2·HTTP/3에서는 멀티플렉싱 덕분에 연결 하나로 수많은 요청을 병렬 처리하므로, 보통 출처당 연결 하나면 충분합니다.
HTTP/1.1 파이프라이닝의 함정
파이프라이닝(pipelining)은 앞 요청의 응답을 기다리지 않고 여러 요청을 연달아 보내는 HTTP/1.1 기능입니다. 이론상 지연을 줄일 것 같지만, 서버가 반드시 보낸 순서대로 응답해야 한다는 제약 때문에 앞의 느린 요청이 뒤의 빠른 응답을 막는 head-of-line blocking이 발생합니다. 게다가 중간 프록시들의 구현이 제각각이라 실전에서 깨지는 경우가 많았습니다. 이런 이유로 브라우저들은 파이프라이닝을 사실상 비활성화했고, 진짜 병렬 처리는 HTTP/2의 멀티플렉싱이 대신하게 되었습니다.
연결이 재사용되는 조건과 타임아웃
연결은 무한정 재사용되지 않으며, 몇 가지 조건이 맞아야 합니다.
- 같은 출처: 스킴·호스트·포트가 같아야 재사용됩니다. HTTPS라면 같은 TLS 파라미터도 요구됩니다.
- idle 타임아웃: 서버·클라이언트 모두 유휴 연결을 일정 시간 뒤 닫습니다(
Keep-Alive: timeout=…). 이 시간이 지나면 다음 요청은 새 연결을 열어야 합니다. - 최대 요청 수:
max=100처럼 한 연결로 처리할 요청 수에 상한을 두기도 합니다. - 경쟁 상태 주의: 서버가 막 닫으려는 연결에 클라이언트가 요청을 보내는 순간이 겹치면 요청이 실패할 수 있어, 클라이언트는 멱등 요청에 한해 새 연결로 자동 재시도하는 것이 안전합니다.
Connection: close: 어느 쪽이든 이 헤더를 보내면 해당 응답 이후 연결을 닫습니다.
결국 좋은 연결 관리란 핸드셰이크라는 고정 비용을 최대한 여러 요청이 분담하게 만드는 일입니다. HTTP/1.1에서는 keep-alive와 다중 연결로, HTTP/2·HTTP/3에서는 멀티플렉싱된 단일 연결로 이를 달성합니다. 클라이언트를 직접 만든다면 커넥션 풀 크기, idle 타임아웃, 재시도 정책을 명시적으로 설정하는 것이 성능과 안정성의 열쇠입니다.