Chapter 1

HTTP 웹의 기초

  • 1.1 HTTP 개관
  • 1.2 URL과 리소스
  • 1.3 HTTP 메시지
  • 1.4 TCP 커넥션

웹의 모든 것은 HTTP로 통한다. 브라우저가 서버에 페이지를 달라고 하든, API가 JSON을 주고받든, 결국 HTTP라는 프로토콜이 그 대화를 중재하고 있지. 이 장에서는 HTTP가 뭔지, URL이 어떻게 생겼는지, 메시지 구조는 어떤지, 그리고 그 밑에 깔린 TCP 커넥션까지 — 웹의 기초 체력을 한번에 잡아보자.

**HTTP(HyperText Transfer Protocol)**는 전 세계 웹 브라우저, 서버, 웹 애플리케이션이 서로 대화할 때 쓰는 공용 언어야. 핵심은 신뢰성 있는 데이터 전송 프로토콜이라는 거거든. JPEG 이미지든 HTML 페이지든 동영상이든, 데이터가 전송 중에 손상되거나 꼬이지 않음을 보장해주니까 개발자는 데이터 무결성 걱정 없이 기능 개발에 집중할 수 있지.

HTTP 통신의 기본 구조는 정말 단순해. 클라이언트가 요청을 보내고 서버가 응답을 돌려주는 것. 크롬이 www.example.com에 접속하면 크롬이 HTTP 클라이언트 역할을 하는 거고, 요청받은 데이터를 저장하고 있다가 돌려주는 쪽이 서버야. 이 한 쌍의 주고받기가 HTTP 트랜잭션의 기본 단위지.

웹 서버가 제공하는 콘텐츠의 원천을 웹 리소스라고 부르는데, 정적 파일만 있는 게 아니야. 사용자가 누군지, 몇 시인지에 따라 다른 콘텐츠를 생성하는 동적 콘텐츠 리소스도 있거든. 검색 엔진, 온라인 쇼핑몰 같은 게 전부 동적 리소스를 제공하는 거지. 그리고 인터넷은 수천 가지 데이터 타입을 다루니까, HTTP는 전송되는 객체 각각에 MIME 타입이라는 라벨을 붙여. text/html이면 HTML 문서, image/jpeg면 JPEG 이미지, application/json이면 JSON 데이터. 브라우저는 이 MIME 타입을 보고 어떻게 처리할지 결정하지.

HTTP 트랜잭션은 요청 명령응답 결과 한 쌍으로 이루어져. 요청에는 HTTP 메서드가 포함되는데, GET은 리소스를 달라는 거고, POST는 데이터를 보내서 처리해달라는 거고, PUT은 저장해달라는 거고, DELETE는 삭제해달라는 거야. 모든 응답에는 상태 코드가 따라오지 — 200이면 OK, 302면 리다이렉트, 404면 못 찾겠다는 뜻이야. 그리고 중요한 건, 하나의 웹페이지를 보여주려면 트랜잭션 하나로는 안 된다는 거야. HTML 받고, 이미지 받고, CSS 받고, JS 받고 — 네이버 메인 페이지 하나 열면 수십에서 수백 개의 HTTP 트랜잭션이 일어나거든.

HTTP에는 여러 버전이 있어. HTTP/0.9는 GET만 지원하던 1991년의 프로토타입이고, HTTP/1.0이 헤더와 상태 코드를 추가한 첫 번째 제대로 된 버전이야. HTTP/1.1은 지속 커넥션이나 파이프라이닝 같은 성능 개선을 담은 버전으로 지금까지도 가장 널리 쓰이고, HTTP/2.0은 바이너리 프레이밍과 멀티플렉싱으로 성능 문제를 해결하려고 등장했지.

웹에는 프락시(클라이언트와 서버 사이의 HTTP 중개자), 캐시(자주 찾는 페이지를 가까이에 보관하는 특별한 프락시), 게이트웨이(다른 프로토콜로 변환해주는 서버), 터널(두 커넥션 사이에서 데이터를 그대로 전달하는 애플리케이션), 에이전트(HTTP 요청을 만드는 클라이언트 프로그램) 같은 구성요소가 있는데, 이것들이 어떻게 동작하는지는 뒤에서 하나씩 다루게 돼.

매일 주소창에 치는 URL이 실제로 어떤 구조를 갖고 있는지 한번 파헤쳐보자. URL은 인터넷 리소스를 가리키는 표준화된 이름이야. FTP면 FTP 방식, Gopher면 Gopher 방식이던 혼란을 정리해서, 어떤 프로토콜이든 통일된 형식으로 리소스를 가리킬 수 있게 만들어준 거지.

서버 리소스 각각은 **URI(Uniform Resource Identifier)**라는 이름을 가져. URI에는 두 종류가 있는데, **URL(Uniform Resource Locator)**은 리소스의 위치를 알려주는 주소고, **URN(Uniform Resource Name)**은 위치와 상관없이 이름으로 식별하는 거야. 현실에서는 거의 모든 URI가 URL이라고 보면 돼.

대부분의 URL은 스킴://사용자이름:비밀번호@호스트:포트/경로;파라미터?질의#프래그먼트라는 9개 컴포넌트를 갖지. 물론 전부 다 쓰는 경우는 드물고, 가장 중요한 세 가지는 스킴, 호스트, 경로야. 스킴은 어떤 프로토콜로 접근할지(http, https, ftp 등), 호스트는 어느 서버에 있는지, 경로는 서버에서 리소스가 어디에 있는지를 가리켜. 파라미터;으로 붙이는 이름/값 쌍이고, 질의 문자열? 뒤에 오는 부분으로 데이터베이스 같은 리소스에 요청 파라미터를 전달하는 용도야. 프래그먼트# 뒤에 오는 부분인데, 중요한 점은 프래그먼트는 서버로 전송되지 않는다는 거야. 브라우저가 전체 리소스를 받은 다음 로컬에서 처리하는 거거든.

매번 전체 URL을 쓸 필요는 없어. HTML 문서 안에서 <a href="./next.html">이라고 쓰면 기저(base) URL을 기준으로 해석되는 상대 URL이 되지. 브라우저는 호스트명 확장(주소창에 google만 치면 www.google.com으로 확장)이나 히스토리 확장(이전 방문 URL 기반 자동 완성) 같은 편의 기능도 제공해.

URL은 안전한 문자들만 사용하도록 설계됐어. US-ASCII 문자 집합의 일부만 쓸 수 있고, 이 범위를 벗어나는 문자는 퍼센트 인코딩으로 처리해야 해. 공백은 %20, 한글 "가"는 %EA%B0%80 이런 식이지. 실무에서 자주 겪는 문제가 검색어에 &=이 들어가면 질의 문자열 파싱이 꼬이는 건데, URL 인코딩을 안 하면 서버가 파라미터를 엉뚱하게 해석하게 돼.

URL의 한계는 위치 기반이라는 점이야. 리소스가 다른 서버로 옮겨지면 URL이 깨지잖아. URN은 이 문제를 해결하기 위해 설계됐는데 — urn:isbn:0596002025 같이 리소스 고유의 이름으로 식별하자는 아이디어지. 근데 아직 널리 채택되지 못해서 당분간 URL이 계속 주류로 남을 거야.

이제 HTTP 메시지의 해부학으로 넘어가자. HTTP 메시지는 단순한 줄 단위 문자열이야. 이진 형식이 아니라 일반 텍스트라서 사람이 읽고 쓰기 편하지. 메시지는 시작줄(이게 어떤 메시지인지 서술), 헤더 블록(메시지의 속성들, 이름: 값 쌍), 본문(실제 데이터, 있을 수도 없을 수도)으로 구성돼. 시작줄과 헤더는 CRLF로 끝나고, 헤더와 본문 사이에 빈 줄 하나가 들어가.

요청 메시지의 시작줄은 GET /index.html HTTP/1.1(메서드 + 요청 URL + 버전)이고, 응답 메시지의 시작줄은 HTTP/1.1 200 OK(버전 + 상태 코드 + 사유 구절)이야. 형식은 다르지만 구조는 같지.

HTTP 메시지의 흐름을 설명할 때 인바운드, 아웃바운드, 업스트림, 다운스트림이라는 방향 용어를 쓰는데, 서버 쪽으로 가는 게 인바운드, 클라이언트 쪽으로 돌아오는 게 아웃바운드야. 메시지는 강물처럼 다운스트림으로 흐르고, 발송자는 항상 수신자의 업스트림이지.

HTTP 메서드는 서버에게 뭘 해달라고 말하는 동사야. GET은 리소스를 달라는 거, HEAD는 GET과 똑같이 동작하는데 헤더만 돌려주는 거(리소스 타입 확인, 존재 여부 확인, 변경 여부 확인에 유용), PUT은 서버에 리소스를 생성하거나 교체하는 거, POST는 데이터를 보내서 알아서 처리해달라는 거, DELETE는 삭제 요청, TRACE는 요청이 서버까지 도달하는 과정에서 어떻게 변경되는지 추적하는 거(보안 이슈로 실무에서는 거의 비활성화), OPTIONS는 특정 리소스에 대해 어떤 메서드가 지원되는지 물어보는 거야. CORS 프리플라이트 요청이 이 OPTIONS 메서드를 쓰거든 — 브라우저가 크로스 오리진 요청을 보내기 전에 먼저 "이 요청 보내도 되나요?"를 확인하는 거지.

상태 코드는 세 자리 숫자로 다섯 그룹으로 나뉘어. 100번대는 정보(100 Continue — 나머지를 계속 보내라), 200번대는 성공(200 OK, 201 Created, 204 No Content), 300번대는 리다이렉션(301 Moved Permanently, 302 Found, 304 Not Modified), 400번대는 클라이언트 에러(400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 405 Method Not Allowed), 500번대는 서버 에러(500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout).

HTTP 헤더는 요청과 응답에 추가 정보를 더해줘. 일반 헤더(Date, Connection, Transfer-Encoding)는 양쪽 모두에 나타나고, 요청 헤더(Host, User-Agent, Accept, If-Modified-Since)는 클라이언트가 서버에 추가 정보를 전달하고, 응답 헤더(Server, Age)는 서버가 부가 정보를 전달하고, 엔터티 헤더(Content-Type, Content-Length, Content-Encoding, Expires)는 본문에 대한 정보를 제공해. HTTP 명세에 정의되지 않은 확장 헤더도 자유롭게 만들어 쓸 수 있어 — X- 접두사가 관례였지만 RFC 6648에서 폐기됐지.

마지막으로, HTTP의 밑바닥에 깔린 TCP 커넥션을 보자. HTTP 성능의 대부분은 TCP 커넥션 관리에 달려 있거든. HTTP는 애플리케이션 계층 프로토콜이라서 네트워크 통신의 세부사항은 TCP/IP에 맡기는데, TCP는 오류 없는 데이터 전송, 순서에 맞는 전달, 조각나지 않는 데이터 스트림을 보장해줘.

HTTP        (애플리케이션 계층)
TCP         (전송 계층)
IP          (네트워크 계층)
네트워크 인터페이스  (데이터 링크 계층)

TCP 커넥션은 발신지 IP, 발신지 포트, 수신지 IP, 수신지 포트 — 이 네 값으로 유일하게 식별돼. HTTP 트랜잭션에서 시간이 어디에 쓰이는지를 알아야 최적화할 수 있는데, 실제로 HTTP 지연의 대부분은 TCP 네트워크 지연 때문이야. 새 TCP 커넥션을 열 때마다 3-way 핸드셰이크(SYN → SYN+ACK → ACK)가 일어나는데, 이 핸드셰이크 자체가 수십에서 수백 밀리초가 걸려. 작은 HTTP 트랜잭션이면 핸드셰이크 시간이 전체 시간의 50% 이상을 차지할 수도 있지. 거기에 확인응답 지연(ACK를 편승시키려고 잠깐 기다리는데 HTTP에서는 편승할 패킷이 마땅치 않아서 순수 지연이 됨), TCP 느린 시작(새 커넥션은 처음에 속도를 제한하다가 점차 올림), 네이글 알고리즘(작은 패킷을 묶어서 보내는데 HTTP에서는 오히려 지연 유발) 같은 것들이 겹치면 성능이 심각하게 떨어질 수 있어.

이 문제를 해결하기 위한 기법으로 병렬 커넥션(여러 TCP 커넥션을 동시에 열어서 요청을 병렬로 보냄, 다만 대역폭이 좁으면 효과 없고 브라우저는 도메인당 6-8개로 제한), 지속 커넥션(트랜잭션이 끝나도 커넥션을 안 닫고 재사용 — HTTP/1.1에서는 이게 기본이야), 파이프라인 커넥션(응답을 기다리지 않고 여러 요청을 연속으로 보내는 방식)이 있어. 파이프라이닝은 멱등 메서드(GET, HEAD)만 가능하고 응답은 요청 순서대로 돌아와야 하는데, HOL(Head-of-Line) 블로킹 문제 — 앞의 응답이 느리면 뒤의 응답도 다 막히는 문제 — 때문에 실질적으로 널리 쓰이지 못했어. 이 문제를 근본적으로 해결한 게 HTTP/2의 멀티플렉싱이지.

커넥션을 끊는 것도 생각보다 복잡한 문제야. HTTP 클라이언트, 서버, 프락시는 언제든 TCP 커넥션을 끊을 수 있거든. 안전하게 끊으려면 먼저 출력 채널을 끊고(절반 끊기), 상대방의 출력 채널이 끊기를 기다린 뒤 양쪽 다 닫아야 해. 입력을 갑자기 끊으면 상대방이 데이터를 보내고 있다가 "connection reset by peer" 에러가 나면서 아직 읽지 않은 버퍼의 데이터도 날아갈 수 있으니까 주의해야 돼.


정리

1장 읽고 기억할 거 세 가지:

  1. HTTP는 텍스트 기반 프로토콜이고, URL로 리소스를 지목하고, 메서드로 행동을 지정하고, 상태 코드로 결과를 알려줘. 메시지는 시작줄 + 헤더 + 본문 구조야
  2. URL = 스킴 + 호스트 + 경로가 핵심 구조이고, 안전하지 않은 문자는 퍼센트 인코딩해야 해. 프래그먼트는 서버로 안 가고 클라이언트에서 처리하는 거야
  3. HTTP 성능 = TCP 커넥션 관리야. 핸드셰이크 지연, 느린 시작 같은 TCP 수준의 지연이 병목이고, HTTP/1.1은 지속 커넥션을 기본으로 써서 이걸 완화하지