테마
HTTP의 응용 2
HTTP의 응용에서 본 로그인 상태와 캐시(cache)에 이어, 다른 사이트에서 시작된 요청을 브라우저가 어떻게 통제하는지 다룬다. 이어서 HTTP의 전송 방식이 버전마다 어떻게 바뀌었는지 본다. HTTP/2는 한 연결 안에서 요청을 동시에 섞어 보내고, HTTP/3는 아래의 전송 계층(transport layer)까지 새 방식으로 바꿨다.
CORS와 동일 출처 정책
브라우저는 동일 출처 정책(same-origin policy)을 지킨다. 출처(origin)는 프로토콜(protocol), 호스트(host), 포트(port)를 합친 것이고, 셋 중 하나라도 다르면 다른 출처다. 이 정책 때문에 https://app.example.com 페이지의 스크립트는 기본적으로 https://api.other.com의 응답을 읽을 수 없다. 악성 페이지가 사용자의 쿠키(cookie)를 실은 요청으로 다른 사이트의 데이터를 몰래 읽는 것을 막기 위해서다.
CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)는 서버가 "이 출처는 내 응답을 읽어도 된다"고 헤더(header)로 허락해 이 제한을 풀어 주는 규칙이다. 서버가 Access-Control-Allow-Origin 헤더에 허용할 출처를 적으면 브라우저가 응답을 스크립트에 넘겨준다.
GET·HEAD·POST에 기본 헤더와 폼·일반 텍스트 형식 본문만 쓰는 요청은 단순 요청이라 바로 보내고, 응답을 스크립트에 넘길지만 CORS 헤더로 정한다. PUT 요청이나 Content-Type: application/json처럼 이 범위를 벗어나는 요청이면, 브라우저는 본 요청 전에 OPTIONS 메서드로 사전 요청(preflight)을 보내 허용 여부부터 묻는다. CORS를 모르는 옛 서버가 예상하지 못한 요청을 받아 데이터를 바꾸는 일을 막기 위해서다.
다이어그램 로딩…
CORS는 브라우저가 지키는 규칙이다. 서버를 보호하는 방화벽(firewall)이 아니므로, curl 같은 도구는 CORS와 상관없이 요청을 보낼 수 있다. 그래서 서버는 인증과 권한 검사를 따로 해야 한다.
HTTP 버전의 발전
HTTP/1.1은 TCP(Transmission Control Protocol, 전송 제어 프로토콜) 연결 하나를 여러 요청에 재사용한다. 파이프라이닝(pipelining)을 쓰면 응답을 기다리지 않고 요청을 연달아 보낼 수 있지만, 응답은 요청 순서대로 와야 한다. 그래서 첫 응답이 느리면 뒤의 응답이 준비돼 있어도 기다리는 HOL 블로킹(Head-of-Line blocking, 선두 차단)이 생긴다. 이 문제 때문에 브라우저들은 파이프라이닝을 기본으로 쓰지 않고, 대신 한 서버에 TCP 연결을 여러 개 열어 병렬로 요청한다.
HTTP/2는 메시지를 텍스트 대신, 길이와 종류를 정해진 자리의 바이트로 적은 바이너리 프레임(frame)으로 잘라 보낸다. 받는 쪽이 경계를 바로 알 수 있어 빠르고 정확하게 나눠 읽는다. 요청과 응답 한 쌍은 번호가 붙은 스트림(stream)이 되고, 여러 스트림의 프레임을 한 TCP 연결 안에서 섞어 보내는 스트림 다중화(multiplexing)가 가능하다. 응답 순서를 기다릴 필요가 없으니 HTTP 층의 HOL 블로킹이 사라진다. 매 요청 비슷하게 반복되는 헤더는 HPACK이라는 헤더 압축 방식으로 줄인다.
하지만 TCP 층의 HOL 블로킹은 남는다. TCP는 연결 전체를 한 줄의 바이트 흐름으로 보고 순서대로 넘겨주므로, 패킷(packet) 하나가 사라지면 그 재전송(retransmission)이 올 때까지 뒤의 데이터를 모두 붙잡아 둔다. 그 패킷과 상관없는 스트림까지 함께 멈추는 것이다.
HTTP/3는 전송 계층을 UDP(User Datagram Protocol, 사용자 데이터그램 프로토콜) 위에서 동작하는 QUIC(UDP 기반 전송 프로토콜)으로 바꿨다. QUIC은 스트림을 전송 계층에서 직접 구분해, 한 스트림의 패킷이 사라져도 그 스트림만 기다리고 나머지는 계속 전달한다. 또 QUIC은 TLS(Transport Layer Security, 전송 계층 보안) 1.3의 핸드셰이크(handshake)를 연결 수립 과정에 합쳐 넣었다. TCP와 TLS를 따로 맺을 때보다 왕복 횟수가 줄어, 첫 요청을 더 빨리 보낼 수 있다. TCP 연결은 IP(Internet Protocol, 인터넷 프로토콜) 주소와 포트 조합으로 구분되므로 주소가 바뀌면 끊긴다. QUIC은 IP 주소와 포트 대신 연결마다 따로 붙인 번호인 연결 ID로 연결을 구분한다. 그래서 와이파이에서 모바일 데이터로 바뀌어 주소가 달라져도 연결을 이어 갈 수 있다. 다만 일부 회사망이나 방화벽은 UDP 트래픽을 막는다. 이런 환경에서 브라우저는 HTTP/3 대신 TCP 위의 HTTP/2로 돌아간다.
세 버전을 한 표로 정리하면 다음과 같다.
| 버전 | 전송 | 핵심 변화 | 남은 문제 |
|---|---|---|---|
| HTTP/1.1 | TCP | 지속 연결(persistent connection), 파이프라이닝 | 응답 순서 대기 |
| HTTP/2 | TCP | 바이너리 프레임, 스트림 다중화, 헤더 압축 | TCP 층 대기 |
| HTTP/3 | UDP 위 QUIC | 스트림별 손실 복구, 빠른 연결 수립 | UDP 차단 환경 |
면접 포인트
- "CORS 오류는 누가 막는 것인가요?" → 브라우저. 서버가
Access-Control-Allow-Origin으로 허락해야 응답을 읽는다. - "HTTP/2가 HOL 블로킹을 해결했나요?" → HTTP 층만. TCP 층 HOL은 남아서 HTTP/3가 QUIC(UDP)으로 옮겼다.
