Back to blog
Nov 23, 2023
6 min read

HTTP 연결 관리 - TCP, Keep-Alive

TCP 연결과 Keep-Alive의 동작 원리

HTTP 연결 관리는 클라이언트와 서버 간의 TCP 연결을 효율적으로 사용하는 방법이다. HTTP는 TCP 위에서 동작하므로, TCP 연결의 생성과 종료 비용이 전체 성능에 큰 영향을 미친다.

초기 HTTP/1.0은 요청마다 새로운 연결을 맺어야 했지만, HTTP/1.1부터 Keep-Alive로 연결을 재사용할 수 있게 되었다. HTTP/2와 HTTP/3는 더 나아가 멀티플렉싱과 연결 풀링으로 성능을 크게 개선했다.

TCP 연결의 비용

브라우저는 HTTP 요청을 보내기 전에 3-Way Handshake를 통해 TCP 연결을 맺어야 하고, 4-Way Handshake로 연결을 종료한다. 이 과정은 생각보다 많은 비용이 든다.

3-Way Handshake

TCP 연결 설정은 3-Way Handshake를 통해 이루어진다.

1. 클라이언트가 서버에 연결 요청을 보낸다 (SYN)
2. 서버가 요청을 수락하고 확인을 보낸다 (SYN-ACK)
3. 클라이언트가 최종 확인을 보낸다 (ACK)

이 과정에서 최소 1.5 RTT(Round Trip Time)가 소요된다. RTT는 패킷이 왕복하는 시간으로, 물리적 거리가 멀수록 길어진다. 서울-뉴욕 간 RTT는 약 150ms이므로, 연결 설정만 225ms가 걸린다.

4-Way Handshake

4-Way Handshake는 연결 종료의 과정이다.

클라이언트 → 서버: FIN
서버 → 클라이언트: ACK
서버 → 클라이언트: FIN
클라이언트 → 서버: ACK

종료 과정에서도 2 RTT가 소요되며, TIME_WAIT 상태로 인해 소켓이 즉시 재사용되지 않는다.

Slow Start

TCP는 처음에 작은 데이터만 보내고, 네트워크 상태를 확인하며 점진적으로 전송량을 늘린다. 이를 Slow Start라고 하며, 초기 전송 속도가 느리다는 의미다.

새로운 연결은 항상 Slow Start부터 시작하므로, 짧은 요청에서는 TCP의 최대 성능을 활용하지 못한다.

HTTP/1.0: 단기 연결

HTTP/1.0은 기본적으로 요청마다 새로운 TCP 연결을 맺고 응답 후 즉시 닫는다.

1. TCP 연결 (3-Way Handshake)
2. HTTP 요청 전송
3. HTTP 응답 수신
4. TCP 연결 종료 (4-Way Handshake)

웹 페이지 하나를 로드하려면 HTML, CSS, 이미지 등 여러 리소스가 필요하다. 10개의 리소스가 있다면 10번의 연결 설정과 종료가 발생한다.

페이지 로딩 시간 = (연결 설정 + 요청 + 응답 + 연결 종료) × 리소스 수

각 연결마다 최소 3.5 RTT가 소요되므로, RTT가 100ms라면 350ms씩 시간이 필요하며, 10개 리소스면 3.5초가 연결 설정에만 사용된다.

HTTP/1.0도 Connection: Keep-Alive 헤더로 연결을 유지할 수 있지만, 기본값이 아니며 서버가 이를 지원해야 한다.

HTTP/1.1: 영구 연결

HTTP/1.1부터 Keep-Alive가 기본값이 되었으며, 한 번 맺은 TCP 연결을 여러 요청에서 재사용한다.

1. TCP 연결 (3-Way Handshake)
2. HTTP 요청 1 전송
3. HTTP 응답 1 수신
4. HTTP 요청 2 전송 (같은 연결)
5. HTTP 응답 2 수신
6. HTTP 요청 3 전송 (같은 연결)
7. HTTP 응답 3 수신
...
N. TCP 연결 종료 (일정 시간 유휴 후)

10개 리소스를 로드해도 TCP 연결은 1번만 설정하면 되므로, 연결 설정 비용이 크게 감소하고 Slow Start도 한 번만 겪으면 된다.

HTTP/1.1에서는 연결을 유지하는 것이 기본이므로, 연결을 닫으려면 Connection 헤더를 통해 이를 명시해야 한다.

Connection 헤더

# 연결 유지 (기본)
GET /api/users HTTP/1.1
Host: example.com
Connection: keep-alive

# 연결 종료
GET /api/logout HTTP/1.1
Host: example.com
Connection: close

서버도 Connection: close를 보내면 응답 후 연결을 닫는다.

Keep-Alive 타임아웃

연결을 무한정 유지할 수 없으므로, 타임아웃을 설정이 필요하다.

HTTP/1.1 200 OK
Connection: keep-alive
Keep-Alive: timeout=5, max=100
  • timeout=5: 5초간 요청이 없으면 연결을 닫는다
  • max=100: 이 연결에서 최대 100개의 요청을 처리한다

웹 서버마다 기본 타임아웃이 다르다. Nginx는 75초, Apache는 5초가 기본이다. 서버가 많은 동시 연결을 유지하면 메모리와 파일 디스크립터가 고갈될 수 있으므로 적절한 값을 설정해야 한다.

HTTP 파이프라이닝

파이프라이닝은 응답을 기다리지 않고 여러 요청을 연속으로 보내는 기법이다.

# 파이프라이닝 없음
요청1 → 응답1 → 요청2 → 응답2 → 요청3 → 응답3

# 파이프라이닝 있음
요청1 → 요청2 → 요청3 → 응답1 → 응답2 → 응답3

요청을 미리 보내므로 네트워크 대기 시간이 줄어들지만, 헤드 오브 라인 블로킹 (HOL Blocking)이라는 치명적인 문제가 존재한다.

헤드 오브 라인 블로킹 (HOL Blocking)

파이프라이닝의 응답은 요청 순서대로 반환되어야 한다. 첫 번째 응답이 지연되면 뒤의 모든 응답도 대기해야 한다.

요청1 (느림) → 요청2 (빠름) → 요청3 (빠름)
  ↓             ↓               ↓
응답1 (5초)  → 응답2 (대기)  → 응답3 (대기)

요청2와 3의 처리가 끝나도, 요청1의 응답이 완료될 때까지 클라이언트는 모든 응답을 받을 수 없다.

이 문제 때문에 파이프라이닝은 실무에서 거의 사용되지 않는다. 대부분의 브라우저는 파이프라이닝을 기본적으로 비활성화한다.

병렬 연결

헤드 오브 라인 블로킹 (HOL Blocking)문제로 인한 파이프라이닝의 대안으로 여러 TCP 연결을 동시에 사용하는 방법이 있다.

연결1: 요청1 → 응답1
연결2: 요청2 → 응답2
연결3: 요청3 → 응답3

브라우저는 일반적으로 도메인당 6개의 동시 연결을 허용하며, 6개 리소스를 병렬로 다운로드할 수 있어 로딩 속도가 빨라진다.

하지만 연결이 많아지면 서버 부하가 증가하고, 네트워크 혼잡도 발생한다. 각 연결마다 Slow Start를 거쳐야 하므로 대역폭을 효율적으로 사용하지 못한다.

HTTP/2: 멀티플렉싱

HTTP/2는 하나의 TCP 연결에서 여러 요청과 응답을 동시에 처리한다.

       TCP 연결

    ┌────┼────┬────┬────┐
    │    │    │    │    │
  요청1 요청2 요청3 요청4 요청5
    │    │    │    │    │
  응답1 응답2 응답3 응답4 응답5

각 요청/응답은 스트림(Stream)이라는 독립적인 흐름으로 전송된다. 스트림은 프레임으로 나뉘어 전송되며, 프레임 단위로 인터리빙된다.

프레임: [요청1] [요청2] [응답1] [요청3] [응답2] [응답3] ...

응답 순서에 제약이 없으므로 헤드 오브 라인 블로킹 문제가 해결돼 요청1이 느려도 요청2의 응답을 먼저 받을 수 있다.

하나의 연결만 사용하므로 연결 설정 비용이 최소화되고, 병렬 연결보다 효율적인 대역폭을 사용한다.

스트림 우선순위

HTTP/2는 스트림에 우선순위를 설정할 수 있다. 중요한 리소스(CSS, JavaScript)를 먼저 전송하여 페이지 렌더링을 빠르게 한다.

헤더 압축 (HPACK)

HTTP/2는 HPACK 알고리즘으로 헤더를 압축해 중복되는 헤더를 기억하고, 변경된 부분만 전송한다. 헤더 크기가 80-90% 감소하여 대역폭을 크게 절약한다.

# 첫 번째 요청
:method: GET
:path: /index.html
host: example.com
user-agent: Mozilla/5.0 ...

# 두 번째 요청 (차이만 전송)
:method: GET
:path: /style.css
# host, user-agent는 이전과 동일하므로 생략

서버 푸시

서버가 클라이언트 요청 없이 리소스를 먼저 보낼 수 있다. HTML을 요청하면 서버가 CSS와 JavaScript를 함께 푸시한다. 하지만 브라우저가 이미 캐시한 파일을 푸시하면 대역폭 낭비가 발생할 수 있어 주의가 필요하다.

클라이언트: GET /index.html
서버:
  - 응답: index.html
  - 푸시: style.css
  - 푸시: script.js

HTTP/3: QUIC

HTTP/3는 TCP 대신 QUIC(UDP 기반) 통신 사용한다.

TCP는 패킷 하나가 손실되면 재전송될 때까지 모든 스트림이 대기하는 TCP 레벨 HOL 블로킹문제가 있다.

TCP: [스트림1-패킷1 손실] → 모든 스트림 대기
QUIC: [스트림1-패킷1 손실] → 스트림1만 대기, 나머지는 계속

QUIC는 각 스트림이 독립적으로 동작하여 패킷 손실의 영향을 최소화해, 연결 설정이 빠르다. TCP + TLS는 2-3 RTT가 필요하지만, QUIC는 0-1 RTT만 필요하다.

TCP + TLS:
  1 RTT (TCP Handshake) + 1 RTT (TLS Handshake) = 2 RTT

QUIC:
  첫 연결: 1 RTT (연결 + TLS)
  재연결: 0 RTT (이전 세션 재사용)

실무에서의 연결 관리

서버의 트래픽 패턴에 따라 Keep-Alive 타임아웃은 조정해야 한다. API 서버는 짧게(5-15초), 정적 콘텐츠 서버는 길게(60-120초) 설정한다.

백엔드에서 외부 API를 호출할 때 연결 풀을 사용해 매번 연결을 생성하지 않고 미리 만들어둔 연결을 재사용하도록 해야한다.

대부분의 최신 브라우저와 서버는 HTTP/2를 지원한다. HTTPS 환경에서 HTTP/2를 활성화하면 성능이 크게 향상된다.

브라우저에서 사용되는 정적 리소스는 CDN을 사용하여 물리적 거리를 줄이고 RTT가 짧아지면 연결 설정 비용이 감소한다.