CS Repo/HTTP 완벽 가이드

HTTP 커넥션 관리 최적화 기법

조금씩 차근차근 2025. 4. 3. 18:12

본 내용은 "HTTP 완벽 가이드" 내용을 참고하여 기록한 정리본입니다.

사전지식: 커넥션 헤더의 오해

커넥션 헤더의 내용

  • 두 개의 인접한 HTTP 애플리케이션 사이에서, “현재 맺고 있는 커넥션”에만 적용할 옵션을 지정하고자 할 때 사용하는 헤더.
  • 예를 들어, 특정한 임시 헤더(meter)나 비표준 옵션(bill-my-credit-card), 연결 종료(close) 등에 대한 정보를 전달한다.

예시로 다음 헤더를 분석해보자.

  • meter 같은 헤더는 다른 커넥션으로 전달되면 안 된다.
  • bill-my-credit-card와 같은 옵션을 선택할 수 있다.
  • 트랜잭션이 끝나면 커넥션을 끊겠다는 의사를 close로 밝힐 수 있다.

커넥션 헤더의 동작 방식

  • HTTP 헤더 필드 명
    • “이 커넥션에만” 해당되는 헤더 이름을 지정한다. 예: Connection: meter
  • 임시적인 토큰 값
    • 특정 비표준 옵션을 적용하기 위한 임시 토큰을 담는다. 예: Connection: bill-my-credit-card
  • close
    • 작업 완료 후 커넥션을 종료할 것임을 알린다.
  • 커넥션 헤더에 지정된 모든 헤더들은 홉 바이 홉(hop-by-hop) 성질을 띠며, 프록시나 게이트웨이를 통해 다음 단계로 전송되어서는 안 된다.

커넥션 헤더에 기술되어 있지 않더라도, hop-by-hop 헤더인 것들

  • Proxy-Authenticate
  • Proxy-Connection
  • Transfer-Encoding
  • Upgrade

이러한 헤더들은 이름 그대로 프록시나 특정 중간 노드와의 연결(hop)에서만 의미가 있는 헤더다.


HTTP 커넥션 관리: HTTP는 어떻게 커넥션을 관리하는가?

순차 트랜잭션 처리의 문제점

  • HTTP 요청마다 커넥션을 맺었다 끊었다 반복한다면, 불필요한 지연이 늘어난다.
  • TCP 연결 시 발생하는 3-way 핸드셰이크, slow start 지연 등은 전체 성능을 떨어뜨린다.
  • 사용자 입장에서는 여러 개의 리소스(예: 이미지)가 동시에 로드되지 않으면 느리게 느낄 수 있다.

커넥션 관리 최적화 기법

  • 병렬 커넥션
    • 여러 개의 TCP 커넥션을 통해 동시에 HTTP 요청을 보내는 방식.
  • 지속 커넥션
    • 한 번 맺은 TCP 커넥션을 여러 HTTP 트랜잭션에 재사용해 연결/해제 오버헤드를 줄이는 방식.
  • 파이프라인 커넥션
    • 하나의 지속 커넥션 위에서 연속된 HTTP 요청들을 순서대로 겹쳐 보내는 방식.
  • 멀티플렉스 커넥션
    • HTTP/2.0부터 도입된 기술로, 단일 커넥션을 여러 스트림으로 나눠 동시에 요청과 응답을 주고받는다.

병렬 커넥션: 병렬로 받기

여러 개의 리소스를 각각의 TCP 커넥션으로 병렬 내려받기

  • 여러 리소스를 각각 독립된 TCP 커넥션으로 동시에 요청함으로써, 병렬 다운로드를 구현한다.

병렬 커넥션의 이점

  • 대역폭이 남아있다면, 나머지 대역폭을 다른 객체 다운로드에 활용할 수 있다.

병렬 커넥션은 브라우저가 여러개의 커넥션을 이용해 서버에게서 데이터를 응답받는 역할을 수행한다.

  • 나중에 설명할, 하나의 커넥션으로 여러 데이터를 조회하는 파이프라인 커넥션과는 다르다.
  • 대역폭에 여유가 있어야 확실한 이점을 누릴 수 있다.

병렬 커넥션의 허점

병렬 커넥션과 서버 간의 트랜잭션을 표현한 그림. 각 커넥션이 따로 동작하게 된다.

  • 대역폭이 부족하면, 병렬 연결을 열어도 성능 이점이 사라진다.
  • 커넥션을 여러 개 열면 그만큼 서버·클라이언트 리소스(메모리, CPU) 소모량이 늘어난다.
  • 불필요하게 많아진 커넥션은 오히려 성능 문제를 초래할 수 있다.

서버는 다양한 클라이언트와 커넥션을 맺어야 함

  • 대부분 브라우저는 특정 서버 또는 프록시당 최대 6개 정도의 병렬 커넥션을 사용한다(HTTP/1.1 기준).
    • 서버 과부하를 줄이기 위함이다.
  • 서버는 특정 클라이언트가 과도한 커넥션을 생성하면, 임의로 커넥션을 끊을 수 있다.

사용자의 체감

  • 여러 다운로드가 동시에 일어나는 것을 눈으로 확인하면, “더 빠르다”는 인상을 준다.
    • 이는 실제로 더 빠르지 않더라도, "사용자가 그렇게 느낀다"는게 중요한 관점이다.
  • 따라서 보통 다중 객체를 병렬 로드하는 방식을 선호하게 된다.

지속 커넥션: 커넥션 연결 과정 단축 1

커넥션을 새로 열면 생기는 문제

  • 사이트 지역성(locality) 개념을 활용하기 어려워진다.
    • 일반적으로 리소스는 한 서버에서 여러개를 받아올 일이 많다.
    • 한 번 연결한 서버에 재접속할 때, 이미 준비된 TCP 상태를 활용하면 빠른 응답이 가능하지만, 매번 새 커넥션을 맺으면 이점을 살리지 못한다.
  • 각 트랜잭션마다 커넥션을 맺고 끊으면, 연결/해제에 드는 시간과 대역폭이 소모된다.
  • 새로운 커넥션은 무조건 TCP slow start로 시작하여, 초기에 성능 저하가 발생한다.
  • 병렬로 열 수 있는 커넥션 수도 제한되어 있다.

튜닝된 커넥션

  • 이미 slow start 단계를 지나 상대적으로 빠른 전송 속도를 낼 수 있는 커넥션을 말한다.
  • “웜(warm) 상태”*의 TCP 커넥션이라 할 수 있으며, 이를 재활용하면 성능상의 이점이 크다.

HTTP/1.0+ Keep-Alive 커넥션

특징

지속 커넥션의 이점을 표현한 그림. 슬로우 스타트로 인한 개선은 표현되지 않았다.

  • 원래 HTTP/1.0에는 명시되지 않은, 비표준 확장 방식으로 시작되었다.
  • Connection: keep-alive 헤더를 사용해, 여러 트랜잭션을 연속 처리할 수 있게 한다.
  • HTTP/1.1의 명세와는 조금 다르며, 구조적 결함이 있어 명확히 표준으로 자리 잡지는 않았다.
  • 예를 들어 Connection: keep-alive, timeout=120, max=5처럼 쉼표로 구분된 옵션을 지정하기도 했다.
    • 5번의 추가 트랜잭션까지 유지
    • 2분 동안 연결 유지

제한과 규칙

  • 중간에 거치는 모든 노드가 Connection: Keep-Alive 헤더를 이해하고 유지해야 한다.
    • 이를 이해하지 못하는 프록시 서버가 끼어 있으면 정상 동작이 어렵다.
  • 클라이언트는 서버가 Connection: Keep-Alive를 보내지 않을 경우, 연결이 끊어질 것임을 인지한다.
  • 커넥션이 끊어지기 전에, 메시지 본문의 끝(길이)을 정확히 알아야 한다.
    • 새 요청이 어디서 시작되는지 파악할 수 있어야, 동일 커넥션을 안전하게 재활용할 수 있다.
  • “정식” HTTP/1.0 기기(비확장)에는 Connection 헤더가 무시될 수 있다.
    • 멍청한(Misbehaving) 프록시가 존재할 가능성.
    • 문제는 클라이언트가 그 기기가 확장 버전(HTTP/1.0+)인지, 순수 HTTP/1.0인지 구분할 방법이 없다는 점.
  • 클라이언트는 응답을 다 못 받은 상황에서 커넥션이 끊어지면, 멱등한 요청이라면 재시도할 수 있어야 한다.

한계 - 멍청한 프록시

  • 특징
    • Connection 헤더를 이해하지 못하여, Keep-Alive가 선언돼도 커넥션을 끊어버리는 문제.
  • 발생 과정

  1. 클라이언트 → 프록시: keep-alive 요청
  2. 프록시 → 서버: keep-alive 요청
  3. 서버 → 프록시: 커넥션 유지 의사
  4. 클라이언트 → 프록시: 같은 커넥션에 새 요청
  5. 프록시는 같은 커넥션에 두 번째 요청이 오는 상황을 인지하지 못한다(혹은 기대치 못한다).
    • 결과적으로 응답이 없거나, 커넥션이 끊긴다.

미봉책: 영리한 프록시와 확장 헤더

영리한 프록시와 멍청한 프록시의 동작 차이

  • Proxy-Authenticate 등의 특별한 확장 헤더 또는 Proxy-Connection: Keep-Alive와 같은 필드로 의사소통할 수 있도록 만든다.
  • 영리한(진보된) 프록시는 이를 보고 멀티 요청 처리를 지원하지만, 여전히 모든 프록시가 이를 지원하는 것은 아니다.
  • 영리한 프록시는 Proxy-Authenticate 등의 특별한 확장 헤더를 인식하고 적절히 처리하지만, 멍청한 프록시는 이를 무시하게 된다.

한계

  • 중간에 멍청한 프록시가 하나라도 끼어 있다면, 다시 문제가 생길 수 있다.

HTTP/1.1 지속 커넥션

특징

  • Keep-Alive가 기본으로 활성화되어 있다.
    • 커넥션을 끊으려면 Connection: close명시적으로 보내야 한다.
  • 하지만, HTTP/1.1에서는 언제든지 클라이언트나 서버가 커넥션을 끊을 수 있으며, 무조건 연결을 영구 유지하는 것은 아니다.

제한과 규칙

  • 커넥션에 있는 모든 메시지는 그 길이를 정확히 알 수 있어야 한다.
  • HTTP/1.1 프록시는 클라이언트와 서버 각각에 대해 별도의 지속 커넥션을 맺고, 이를 관리해야 한다(홉 바이 홉).
  • 어느 한쪽에서든 커넥션은 예고 없이 끊길 수 있다.
    • 클라이언트는 멱등한 요청이면 다시 보낼 준비가 되어 있어야 한다.
  • 하나의 클라이언트는, 한 서버에 대해 일반적으로 최대 2개의 지속 커넥션만 유지해야 한다(서버 과부하 방지 차원).

파이프라인 커넥션: 커넥션 연결 과정 단축 2

참고) 파이프라인은 현재 HoL(Head of Line) 블로킹 문제때문에, 기본으로 비활성화되어 있다.

순차 커넥션, 지속 커넥션, 파이프라인 지속 커넥션 비교 이미지

제약과 규칙

  • 클라이언트는 해당 커넥션이 지속 커넥션인지 확인된 이후에만 파이프라인을 사용할 수 있다.
  • 응답 순서는 요청 순서와 동일해야 한다.
    • 순서가 뒤섞이면 클라이언트가 요청-응답 매칭을 할 수 없다.
  • 커넥션이 언제 끊어져도, 아직 완료되지 않은 요청이 있다면 재시도할 준비가 되어 있어야 한다.
  • 비멱등(non-idempotent) 요청은 파이프라인으로 보내면 안 된다.
    • 끊어졌을 때 재요청 가능 여부가 명확하지 않기 때문이다.
    • 예: POST가 대표적인 비멱등 요청.
더보기

참고) HoL 블로킹 (Head of Line Blocking)이란?

- 파이프라인은 응답 순서가 요청 순서와 동일해야 하는데, 그렇게 되면 반드시 첫번째 요청부터 먼저 처리해야 한다.

- 그런데 첫번째 요청이 매우 오래걸리면, 금방 끝나는 뒷 요청까이 오랫동안 기다리다가 타임아웃이 발생하는 사태가 일어날 수 있다.

- 이를 HOL 블로킹이라고 하며, HTTP/2.0에서는 HTTP 멀티플렉싱으로 파이프라인이 해결하려던 문제를 해결하였다.


HTTP/1.1 "커넥션 끊기"에 대한 관련 주제

막 끊어도 됨

  • HTTP/1.1 표준상, 특정 시점에 커넥션을 끊으면 안 된다는 강제 규정은 없다.
  • 다만 요청 또는 응답 중간에 끊으면, 해당 트랜잭션은 실패하고 재시도가 필요할 수 있다.

Content-Length, Truncation

  • 지속 커넥션 환경에서 정확한 본문 길이(Content-Length)를 모르면, 다음 요청·응답이 어디서 시작되는지 구분하기 어렵다.
  • 트렁킹(Chunked Transfer-Encoding)처럼 크기를 알 수 없는 방식이라면, 각 청크를 명확히 구분해야 한다.
  • 안전을 위해, 수신자는 데이터 길이가 의심스러울 경우 업스트림에 길이를 다시 요청해 확인해야 한다.

커넥션 끊기 방식 - 우아한 커넥션 끊기에 대하여

전체 끊기(full-close)

  • 전체 끊기
    • 입력·출력 스트림을 모두 닫아버리는 방식.

입력 끊기 (half-close)
출력 끊기 (half-close)

  • 절반 끊기(half-close)
    • 입력 채널과 출력 채널 중 한쪽만 닫고, 반대편이 닫히길 기다리는 것을 의미한다.
    • 입력을 먼저 닫고, 출력이 닫히길 기다리거나, 출력을 먼저 닫고, 입력이 닫히길 기다리는 것이 가능하다.
  • 주의
    • 서버가 입력을 닫은 상태에서 클라이언트가 데이터를 보내면 서버의 운영체제 차원에서 connection reset by peer 에러를 메시지로 전달한다.
    • 이 오류를 받은 클라이언트 운영체제의 동작은, 이미 도착한 데이터가 있더라도 함께 폐기하기 때문에, 아직 수신 버퍼에 남은 데이터도 폐기될 수 있다.
    • 파이프라인 커넥션이 있을 때 이 문제가 함께 발생하면 심각해질 수 있기에, "입력 끊기"는 잘 사용되지 않는다.
  • 실제로는, 출력을 먼저 닫고, 수신만 마저 받는 것이 비교적 안전한 방법으로 여겨진다.
    • 이를 “우아한 커넥션 끊기” 라고 한다.

참고) 멱등성

  • 연산이나 요청을 여러 번 수행해도 결과가 처음 한 번 수행한 것과 동일하게 유지되는 성질.
  • 멱등성 보장 방법
    1. 멱등한 설계
      • 로직 자체를 여러 번 요청해도 서버 상태가 변하지 않도록 만드는 방법.
    2. 멱등 키
      • UUID 같은 고유 식별자 키를 생성하고, 해당 키로 중복 요청을 방어한다.
      • 한 번 처리된 멱등 키에 대해서는 일정 기간 후에만 다시 허용하거나, 아예 차단하는 구조를 만든다.

HTTP에서는 이러한 멱등성을 고려해, 지속 커넥션에서 요청이 끊겼을 때 다시 보낼 수 있는지 여부를 판단한다. 이 원리가 잘 지켜지면, 의도치 않은 중복 처리나 오류를 줄일 수 있다.