Chapter 2

HTTP 아키텍처

  • 2.1 웹 서버
  • 2.2 프락시
  • 2.3 캐시
  • 2.4 게이트웨이와 터널
  • 2.5 웹 로봇
  • 2.6 HTTP/2.0

HTTP는 클라이언트와 서버만으로 끝나지 않아. 그 사이에 프락시, 캐시, 게이트웨이 같은 중개자들이 끼어들면서 보안, 성능, 확장성을 만들어내지. 이 장에서는 웹 서버의 내부 동작부터 프락시, 캐시, 게이트웨이, 웹 로봇, 그리고 HTTP/2.0까지 — HTTP 아키텍처의 전체 그림을 그려보자.

웹 서버는 형태가 다양해. Apache, Nginx 같은 범용 소프트웨어 웹 서버, 프린터 관리 인터페이스 같은 곳에 내장되는 임베디드 웹 서버, 전용 하드웨어에서 동작하는 하드웨어 웹 서버가 있지. 하지만 공통점은 하나야 — HTTP 요청을 받아서 응답을 돌려주는 것.

실제 웹 서버가 HTTP 요청을 처리하는 과정은 7단계 파이프라인을 따르거든. 커넥션 수락 → 요청 수신 → 요청 처리 → 리소스 매핑 → 응답 생성 → 응답 전송 → 로깅. 클라이언트가 TCP 커넥션을 요청하면 서버가 수락하고, 요청 메시지를 파싱해서 메서드, URL, 헤더를 추출해. 좋은 웹 서버는 파싱 후 내부적으로 다루기 쉬운 자료구조에 저장하지 — URL을 호스트, 포트, 경로, 쿼리 등으로 분해하고, 헤더를 해시 테이블 같은 곳에 넣는 식으로.

서버마다 동시 커넥션을 처리하는 방식이 다른데, 이게 성능을 결정해. 단일 스레드는 한 번에 하나만 처리하니까 성능이 나쁘고, 멀티프로세스/멀티스레드는 요청마다 프로세스나 스레드를 할당하는 방식(Apache prefork MPM이 대표적)이야. 다중 I/O는 하나의 스레드가 여러 커넥션을 동시에 감시하고 활동이 있는 커넥션만 처리하는 이벤트 루프 기반이고 — Nginx가 이 방식이지. 멀티스레드 다중 I/O는 여러 스레드가 각각 다중 I/O를 돌려서 가장 확장성이 좋아.

리소스 매핑이 핵심 단계야. 가장 단순한 형태는 URL 경로를 서버 파일 시스템의 **문서 루트(docroot)**에 대응시키는 거지. /index.html 요청이 오면 /var/www/html/index.html 파일을 가리키는 식. 가상 호스팅에서는 Host 헤더를 보고 어떤 문서 루트를 쓸지 결정하고, 동적 콘텐츠는 CGI나 서블릿, PHP, Node.js 같은 서버 사이드 기술에 매핑해. MIME 타입 결정도 여기서 하는데, 파일 확장자(가장 흔한 방식), 매직 타이핑(파일 내용 패턴으로 판단), 명시적 타이핑(관리자가 직접 설정), 내용 협상(클라이언트와 협상) 방식이 있어. 응답을 만들고 보낸 다음에는 트랜잭션 로그를 남기지.

**프락시(proxy)**는 클라이언트와 서버 사이에 끼어서 온갖 일을 하는 HTTP 중개자야. 클라이언트 입장에서는 서버처럼 보이고, 서버 입장에서는 클라이언트처럼 보이지. 프락시는 같은 프로토콜을 중개하는 거고, 게이트웨이는 서로 다른 프로토콜을 연결해주는 건데, 실무에서는 이 경계가 꽤 모호해.

프락시를 쓰는 이유가 다양해. 어린이 필터(교육 기관에서 성인 콘텐츠 차단), 문서 접근 제어자(중앙 집중식 접근 제어), 보안 방화벽(트래픽 감시 및 악성 콘텐츠 필터링), 웹 캐시(인기 문서 사본을 저장해서 성능 향상), 대리 프락시(리버스 프락시)(웹 서버인 척 위장해서 부하 분산이나 콘텐츠 라우팅), 트랜스코더(GIF를 JPG로 바꾸거나 HTML 축소), 익명화 프락시(IP, User-Agent 같은 신원 정보를 제거해서 개인정보 보호) 등이 있지.

프락시는 네트워크의 다양한 위치에 배치돼. 출구 프락시는 로컬 네트워크 출구에서 밖으로 나가는 트래픽을 제어하고, 접근 프락시는 ISP 접근 지점에서 캐시로 성능을 개선하고, 대리 프락시는 웹 서버 바로 앞에서 보안과 성능을 담당하고, 네트워크 교환 프락시는 네트워크 사이의 피어링 교환 지점에 위치하지. 프락시는 연쇄적으로 구성할 수도 있어 — 클라이언트 → 프락시1 → 프락시2 → 서버 식의 프락시 계층을 만들 수 있고, 동적 부모 선택을 통해 요청 내용에 따라 다른 부모 프락시로 보낼 수도 있지.

프락시에 요청할 때는 서버에 직접 요청할 때와 달리 완전한 URI를 보내야 해. 서버에는 GET /index.html HTTP/1.1이라고 보내지만, 프락시에는 GET http://www.example.com/index.html HTTP/1.1이라고 보내는 거야. 프락시가 여러 서버를 대상으로 하니까 어떤 서버에 접속해야 할지 알려줘야 하거든. 인터셉트 프락시(투명 프락시)는 클라이언트가 프락시 존재를 모르니까 부분 URI를 받게 되는데, 이 경우 Host 헤더나 인터셉트한 IP/포트 정보로 목적지를 알아내지.

Via 헤더는 메시지가 지나는 각 중간 노드의 정보를 나열해서 메시지 전달을 추적하는 데 쓰여. Via: 1.1 proxy-a.example.com, 1.0 cache.isp.net 이런 식이지. Via 목록에 자기가 이미 있으면 루프가 발생한 거라고 판단할 수도 있어. 프락시가 이해하지 못하는 헤더 필드는 반드시 그대로 전달해야 하고, 같은 이름의 헤더가 여러 개 있으면 상대적 순서도 유지해야 해.

캐시는 자주 요청되는 문서의 사본을 저장해서 원 서버까지 안 가고도 응답할 수 있게 해주는 장치야. 웹 성능에 있어서 가장 중요한 요소 중 하나지. 캐시가 없으면 같은 문서를 여러 클라이언트가 요청할 때마다 서버가 매번 같은 데이터를 보내야 하고, 대역폭 병목도 심해지고, 갑자기 뉴스가 터져서 요청이 폭주하는 Flash Crowd 상황에서 서버가 다운될 수도 있거든.

캐시에 요청된 문서의 사본이 있으면 캐시 적중(cache hit), 없으면 **캐시 부적중(cache miss)**이야. 캐시된 사본이 아직 신선한지 확인하는 걸 재검사라고 하는데, 콘텐츠가 변경되지 않았으면 서버가 304 Not Modified로 응답하고(이게 재검사 적중), 변경됐으면 새 콘텐츠를 전체로 보내줘. 가장 많이 쓰는 재검사 도구가 If-Modified-Since 헤더야. 캐시 적중률이 40%만 돼도 꽤 좋은 편이라고 하고, 크기가 큰 문서를 캐시에서 제공하는 바이트 적중률이 더 정확한 지표일 수 있어.

캐시 토폴로지를 보면, **개인 전용 캐시(private cache)**는 브라우저에 내장된 것처럼 한 사용자에게만 할당되고, **공용 캐시(public cache)**는 여러 사용자가 공유하는 프락시 캐시야. 캐시도 계층 구조로 구성할 수 있고(1단계 로컬 → 2단계 지역 → 원 서버), 현대 캐시 네트워크는 단순 계층이 아니라 캐시들끼리 서로 대화하는 캐시망을 구성해서 형제 캐시에 물어보거나 동적으로 라우팅을 결정하기도 하지.

서버는 Expires 헤더나 Cache-Control: max-age 헤더로 문서에 유통기한을 붙여. 만료되면 재검사를 해야 하는데, If-Modified-Since는 날짜 기반이고, If-None-MatchETag(엔터티 태그) 기반이야. ETag가 더 정확한 이유는 — 문서가 주기적으로 재생성되지만 내용은 안 바뀌는 경우, 1초보다 짧은 간격으로 변경되는 경우에 날짜만으로는 감지가 안 되거든.

Cache-Control 헤더로 캐시 행동을 세밀하게 제어할 수 있어. no-store는 "아예 저장하지 마", no-cache는 "저장은 해도 되지만 쓰기 전에 반드시 서버에 확인해", must-revalidate는 만료된 사본을 재검사 없이 제공하면 안 된다는 거, max-age는 신선도 유지 시간, public/private는 공용/전용 캐시 저장 여부를 지정하지. no-cacheno-store를 헷갈리는 사람이 많은데, no-store가 진짜 저장 금지고 no-cache는 매번 검증하라는 뜻이야.

게이트웨이는 HTTP 트래픽을 다른 프로토콜로 변환하는 역할을 해. HTTP/FTP 게이트웨이는 HTTP 요청을 받아서 FTP 프로토콜로 문서를 가져온 뒤 HTTP 응답으로 돌려주지. HTTP/FTP처럼 슬래시로 표기하는데, 왼쪽이 클라이언트 측, 오른쪽이 서버 측 프로토콜이야. **CGI(Common Gateway Interface)**는 최초의 서버 확장 메커니즘으로, 웹 서버가 외부 프로그램을 실행해서 동적 콘텐츠를 생성하는 표준 인터페이스야. 요청마다 새 프로세스를 생성하는 비용이 큰 게 문제인데, Fast CGI가 프로세스를 미리 만들어놓고 재사용하는 방식으로 이걸 개선했지. SOAP은 HTTP를 전송 수단으로 사용해서 XML 메시지를 교환하는 프로토콜이야.

웹 터널은 HTTP 커넥션을 통해 HTTP가 아닌 트래픽을 전송할 수 있게 해줘. 가장 대표적인 용도가 HTTPS 트래픽을 HTTP 프락시를 통해 전달하는 거야. CONNECT 메서드로 설정하는데, 프락시가 대상 서버에 TCP 커넥션을 맺고 200 Connection Established를 보낸 후부터 데이터를 맹목적으로 양방향 전달해. 프락시가 내용을 해석하지 않으니까 SSL 암호화된 데이터도 통과할 수 있는 거지. 다만 악의적인 트래픽이 터널을 통해 방화벽을 우회할 수 있으니까, 터널은 잘 알려진 포트(443 같은)로만 연결되도록 제한하는 게 좋아.

HTTP 릴레이는 HTTP를 완전히 이해하지 못하는 간단한 프락시인데, Connection 헤더를 이해 못 하고 그대로 전달하면 keep-alive 핸드셰이크가 엉망이 되어 커넥션이 멈추는 dumb proxy 문제를 일으킬 수 있어. 조심해야 해.

**웹 크롤러(web crawler)**는 웹페이지를 가져오고, 링크를 추출해서, 그 링크의 페이지를 또 가져오고... 이걸 재귀적으로 반복하면서 웹을 탐색하는 로봇이야. 출발점 URL의 초기 집합을 루트 집합이라 하고, 순환 링크에 빠지지 않으려면 이미 방문한 URL을 기록해야 해. 웹 규모가 엄청나니까 블룸 필터 같은 확률적 자료구조로 "이 URL을 방문한 적 있는가"를 빠르게 판단하기도 하지. 더 심각한 건 로봇 함정이야 — /calendar/2024/01/01/next/next/next/... 같은 URL이 끝없이 이어지면 크롤러가 빠져나올 수 없거든.

예의 바른 로봇은 robots.txt를 먼저 읽고 지시를 따라야 해. User-agent: *Disallow: /private/ 같은 규칙으로 서버 관리자가 로봇의 접근을 제어하는 거야. 개별 페이지에서는 <meta name="robots" content="noindex, nofollow">로 인덱싱과 링크 추적을 막을 수도 있지. 좋은 로봇은 요청 속도를 제한하고, User-AgentFrom 헤더로 연락처를 제공하고, 조건부 요청(If-Modified-Since)으로 서버 부하를 줄여. 검색엔진 크롤러는 수집한 문서의 단어들을 전문 인덱스(역색인)에 추가하고, 사용자가 검색하면 순위를 매겨 결과를 보여주는데 — Google의 PageRank가 대표적인 순위 알고리즘이지.

마지막으로 HTTP/2.0. HTTP/1.1은 1999년에 표준화된 이후 웹의 근간이었지만, 현대 웹의 복잡성 앞에서 성능 한계가 드러났어. 가장 큰 문제는 회선 지연이었거든 — 하나의 커넥션에서 요청을 보내고 응답을 받아야 다음 요청을 보낼 수 있으니까. 개발자들이 도메인 샤딩(리소스를 여러 도메인에 분산), 스프라이트 이미지, 인라이닝, 번들링 같은 꼼수를 썼지만 근본적 해결이 아니었지. Google이 SPDY를 실험적으로 만들어서 유의미한 성능 향상을 보여줬고, 이걸 기반으로 2015년에 HTTP/2.0이 RFC 7540으로 표준화됐어.

HTTP/2.0의 핵심은 전송 성능을 극대화하면서 HTTP의 의미론은 유지하는 것이야. GET, POST, 상태 코드, 헤더 같은 개념은 그대로인데, 메시지를 전선에 실어 보내는 방식만 바뀐 거지. HTTP/1.1은 텍스트 기반이라 파싱이 느렸는데, HTTP/2.0은 바이너리 프레이밍 계층을 도입해서 모든 메시지를 이진 형식의 프레임으로 인코딩해.

가장 중요한 개선은 멀티플렉싱이야. HTTP/1.1의 HOL 블로킹 — 앞선 요청의 응답이 안 오면 뒤의 요청들이 다 대기하는 문제 — 을 해결한 거지. HTTP/2.0은 하나의 TCP 커넥션 안에서 여러 스트림을 동시에 열 수 있어. 프레임들이 인터리빙되어 전송되니까 하나의 큰 응답이 다른 응답을 블로킹하지 않아. 커넥션 하나로 다 처리하니까 도메인 샤딩 같은 꼼수가 필요 없어졌지.

HPACK 헤더 압축 알고리즘은 헤더 필드를 인덱스로 치환하고 이전에 보냈던 헤더와의 차이만 보내서 헤더 크기를 85-88% 줄여. 서버 푸시는 서버가 클라이언트 요청 없이도 리소스를 미리 보내는 기능이고, 각 스트림에 우선순위를 부여해서 CSS를 이미지보다 먼저 전송하도록 힌트를 줄 수도 있어. 스트림 수준의 흐름 제어로 특정 스트림이 대역폭을 독점하지 않게 하는 것도 가능하지.

보안 이슈로는, HTTP/2.0 메시지를 HTTP/1.1로 변환하는 프락시를 이용한 중개자 캡슐화 공격 가능성과, 커넥션을 오래 유지하는 특성 때문에 사용자 행동 추적에 악용될 수 있는 개인정보 누출 문제가 있어.


정리

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

  1. 프락시는 보안, 캐시, 필터링, 부하 분산 등 다양한 목적으로 쓰이고, Via 헤더로 메시지 경로를 추적해. 캐시는 Cache-Control 지시자(no-store, no-cache, max-age)로 행동을 제어하고, 신선도는 만료 + 재검사(ETag/If-Modified-Since)로 관리하지
  2. 게이트웨이는 프로토콜 변환기, 터널은 CONNECT로 맹목적 전달이야. 웹 로봇은 robots.txt를 존중하고 요청 속도를 제한하는 게 기본 에티켓이고
  3. HTTP/2.0은 멀티플렉싱이 핵심이야. 하나의 TCP 커넥션에서 여러 스트림을 동시에 처리해서 HOL 블로킹을 해결하고, HPACK 압축과 서버 푸시로 추가 성능 향상을 제공하지